Initial commit

This commit is contained in:
2025-04-27 14:31:03 +02:00
commit fc15f362cf
11 changed files with 3019 additions and 0 deletions
+322
View File
@@ -0,0 +1,322 @@
/**
* Main Application Module
* Handles UI interactions and coordinates between modules
*/
class HealthAnalyticsApp {
constructor() {
// Initialize UI elements
this.initializeUI();
// Add event listeners
this.addEventListeners();
}
/**
* Initialize UI elements
*/
initializeUI() {
// File upload elements
this.metricsFileInput = document.getElementById('metrics-file');
this.workoutsFileInput = document.getElementById('workouts-file');
this.metricsFilename = document.getElementById('metrics-filename');
this.workoutsFilename = document.getElementById('workouts-filename');
// Analysis options elements
this.analysisFocusSelect = document.getElementById('analysis-focus');
this.recommendationStyleSelect = document.getElementById('recommendation-style');
this.reportDetailSelect = document.getElementById('report-detail');
this.customPromptTextarea = document.getElementById('custom-prompt');
this.showGptInputCheckbox = document.getElementById('show-gpt-input');
// Buttons
this.useSampleDataButton = document.getElementById('use-sample-data');
this.generateReportButton = document.getElementById('generate-report');
this.downloadReportButton = document.getElementById('download-report');
this.newAnalysisButton = document.getElementById('new-analysis');
// Report section
this.reportSection = document.querySelector('.report-section');
}
/**
* Add event listeners to UI elements
*/
addEventListeners() {
// File input change events
this.metricsFileInput.addEventListener('change', (e) => this.handleFileInputChange(e, this.metricsFilename));
this.workoutsFileInput.addEventListener('change', (e) => this.handleFileInputChange(e, this.workoutsFilename));
// Button click events
this.useSampleDataButton.addEventListener('click', () => this.handleUseSampleData());
this.generateReportButton.addEventListener('click', () => this.handleGenerateReport());
this.downloadReportButton.addEventListener('click', () => this.handleDownloadReport());
this.newAnalysisButton.addEventListener('click', () => this.handleNewAnalysis());
}
/**
* Handle file input change
* @param {Event} event - Change event
* @param {HTMLElement} filenameElement - Element to display filename
*/
handleFileInputChange(event, filenameElement) {
const file = event.target.files[0];
if (file) {
filenameElement.textContent = file.name;
} else {
filenameElement.textContent = 'No file selected';
}
}
/**
* Handle use sample data button click
*/
async handleUseSampleData() {
try {
// Disable the button during loading
this.useSampleDataButton.disabled = true;
this.useSampleDataButton.textContent = 'Loading...';
// Load sample data
await dataProcessor.loadSampleData();
// Update UI to show sample data is loaded
this.metricsFilename.textContent = 'Sample metrics.csv loaded';
this.workoutsFilename.textContent = 'Sample workouts.csv loaded';
// Show success message
this.showNotification('Sample data loaded successfully!', 'success');
} catch (error) {
console.error('Error loading sample data:', error);
this.showNotification('Error loading sample data: ' + error.message, 'error');
} finally {
// Re-enable the button
this.useSampleDataButton.disabled = false;
this.useSampleDataButton.textContent = 'Use Sample Data';
}
}
/**
* Handle generate report button click
*/
async handleGenerateReport() {
try {
// Check if files are selected but not processed
if ((this.metricsFileInput.files.length > 0 || this.workoutsFileInput.files.length > 0) &&
(!dataProcessor.metricsData || !dataProcessor.workoutsData)) {
// Process the uploaded files
await this.processUploadedFiles();
}
// Check if data is loaded
else if (!this.isDataLoaded()) {
// If no files are selected, try to use sample data
if (this.metricsFilename.textContent === 'No file selected' &&
this.workoutsFilename.textContent === 'No file selected') {
await this.handleUseSampleData();
} else {
throw new Error('Please upload both metrics and workouts files or use sample data.');
}
}
// Disable the button during generation
this.generateReportButton.disabled = true;
// Get analysis options
const options = this.getAnalysisOptions();
// Prepare data for analysis
const data = dataProcessor.prepareDataForAnalysis();
// Generate the report
await reportGenerator.generateReport(data, options);
} catch (error) {
console.error('Error generating report:', error);
this.showNotification('Error generating report: ' + error.message, 'error');
} finally {
// Re-enable the button
this.generateReportButton.disabled = false;
}
}
/**
* Handle download report button click
*/
async handleDownloadReport() {
try {
await reportGenerator.generatePDF();
} catch (error) {
console.error('Error downloading report:', error);
this.showNotification('Error downloading report: ' + error.message, 'error');
}
}
/**
* Handle new analysis button click
*/
handleNewAnalysis() {
// Reset the report view
reportGenerator.resetReport();
// Scroll to the top of the page
window.scrollTo({ top: 0, behavior: 'smooth' });
}
/**
* Check if data is loaded
* @returns {boolean} - Whether data is loaded
*/
isDataLoaded() {
return dataProcessor.metricsData !== null && dataProcessor.workoutsData !== null;
}
/**
* Get analysis options from UI
* @returns {Object} - Analysis options
*/
getAnalysisOptions() {
// Get API key from local storage or prompt
let apiKey = localStorage.getItem('openai_api_key');
// If no API key is stored, prompt the user
if (!apiKey) {
apiKey = this.promptForAPIKey();
}
return {
analysisFocus: this.analysisFocusSelect.value,
recommendationStyle: this.recommendationStyleSelect.value,
reportDetail: this.reportDetailSelect.value,
customPrompt: this.customPromptTextarea.value,
showGptInput: this.showGptInputCheckbox.checked,
apiKey: apiKey
};
}
/**
* Prompt user for OpenAI API key
* @returns {string} - API key
*/
promptForAPIKey() {
// In a real application, you would use a modal dialog
// For this demo, we'll use a simple prompt
const apiKey = prompt(
'Enter your OpenAI API key to generate personalized recommendations.\n' +
'If you don\'t have an API key, leave this blank to use demo mode.',
''
);
// Ask if the user wants to save the API key
if (apiKey && apiKey.trim()) {
const saveKey = confirm('Would you like to save this API key for future use?');
if (saveKey) {
localStorage.setItem('openai_api_key', apiKey);
}
}
return apiKey || '';
}
/**
* Process uploaded files
* @returns {Promise} - Promise resolving when files are processed
*/
async processUploadedFiles() {
const metricsFile = this.metricsFileInput.files[0];
const workoutsFile = this.workoutsFileInput.files[0];
if (!metricsFile || !workoutsFile) {
throw new Error('Please upload both metrics and workouts files.');
}
return await dataProcessor.processFiles(metricsFile, workoutsFile);
}
/**
* Show notification message
* @param {string} message - Message to display
* @param {string} type - Notification type (success, error, warning)
*/
showNotification(message, type = 'info') {
// Create notification element
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.textContent = message;
// Add close button
const closeButton = document.createElement('button');
closeButton.className = 'notification-close';
closeButton.innerHTML = '×';
closeButton.addEventListener('click', () => {
document.body.removeChild(notification);
});
notification.appendChild(closeButton);
// Add to document
document.body.appendChild(notification);
// Auto-remove after 5 seconds
setTimeout(() => {
if (document.body.contains(notification)) {
document.body.removeChild(notification);
}
}, 5000);
}
}
// Initialize the application when the DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
// Create notification styles dynamically
const notificationStyles = document.createElement('style');
notificationStyles.textContent = `
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 20px;
border-radius: 6px;
color: white;
max-width: 300px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
z-index: 1000;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.notification.success {
background-color: #2ecc71;
}
.notification.error {
background-color: #e74c3c;
}
.notification.warning {
background-color: #f39c12;
}
.notification.info {
background-color: #3498db;
}
.notification-close {
background: none;
border: none;
color: white;
font-size: 20px;
cursor: pointer;
position: absolute;
top: 5px;
right: 10px;
}
`;
document.head.appendChild(notificationStyles);
// Initialize the app
window.app = new HealthAnalyticsApp();
});
+700
View File
@@ -0,0 +1,700 @@
/**
* Charts Module
* Handles the creation and rendering of data visualizations for health reports
*/
class ChartsManager {
constructor() {
// Store references to created charts for cleanup
this.charts = [];
// Define chart colors
this.colors = {
primary: '#4a6fa5',
secondary: '#6fb98f',
accent: '#ff7e67',
dark: '#2c3e50',
light: '#ecf0f1',
sleep: {
total: '#4a6fa5',
deep: '#2c3e50',
light: '#95a5a6',
rem: '#3498db',
awake: '#e74c3c'
},
stress: {
calm: '#2ecc71',
balanced: '#3498db',
stressful: '#f39c12',
veryStressful: '#e74c3c'
}
};
}
/**
* Create all charts for the report
* @param {Object} data - Processed health and workout data
* @param {HTMLElement} container - Container element for the charts
*/
createCharts(data, container) {
// Clear any existing charts
this.clearCharts();
// Create chart sections
const sleepSection = this.createChartSection('Sleep Patterns', container);
const stressSection = this.createChartSection('Stress Levels', container);
const workoutSection = this.createChartSection('Workout Analysis', container);
const hrvSection = this.createChartSection('Recovery Metrics', container);
// Create charts
this.createSleepChart(data.metrics, sleepSection);
this.createStressChart(data.metrics, stressSection);
this.createWorkoutChart(data.workouts, workoutSection);
this.createHRVChart(data.metrics, hrvSection);
}
/**
* Create a section for a chart
* @param {string} title - Section title
* @param {HTMLElement} container - Parent container
* @returns {HTMLElement} - The charts wrapper element
*/
createChartSection(title, container) {
const section = document.createElement('div');
section.className = 'chart-section';
const heading = document.createElement('h3');
heading.textContent = title;
section.appendChild(heading);
// Create a wrapper for all charts in this section
const chartsWrapper = document.createElement('div');
chartsWrapper.className = 'charts-wrapper';
section.appendChild(chartsWrapper);
container.appendChild(section);
return chartsWrapper;
}
/**
* Create an individual chart container
* @param {HTMLElement} wrapper - Parent wrapper element
* @returns {HTMLElement} - The chart container element
*/
createChartContainer(wrapper) {
const chartContainer = document.createElement('div');
chartContainer.className = 'chart-container';
wrapper.appendChild(chartContainer);
return chartContainer;
}
/**
* Create sleep patterns chart
* @param {Array} metricsData - Processed metrics data
* @param {HTMLElement} wrapper - Wrapper for the chart containers
*/
createSleepChart(metricsData, wrapper) {
// Filter out days without sleep data
const sleepData = metricsData.filter(day =>
day.metrics['Sleep Hours'] !== undefined
);
if (sleepData.length === 0) {
this.showNoDataMessage(wrapper, 'No sleep data available');
return;
}
// Prepare data for chart
const dates = sleepData.map(day => day.date);
const totalSleep = sleepData.map(day => day.metrics['Sleep Hours']);
const deepSleep = sleepData.map(day => day.metrics['Time In Deep Sleep'] || 0);
const lightSleep = sleepData.map(day => day.metrics['Time In Light Sleep'] || 0);
const remSleep = sleepData.map(day => day.metrics['Time In REM Sleep'] || 0);
// Create container for stacked bar chart
const stackedContainer = this.createChartContainer(wrapper);
const canvas = document.createElement('canvas');
stackedContainer.appendChild(canvas);
// Create chart
const chart = new Chart(canvas, {
type: 'bar',
data: {
labels: dates,
datasets: [
{
label: 'Deep Sleep',
data: deepSleep,
backgroundColor: this.colors.sleep.deep,
stack: 'sleep'
},
{
label: 'Light Sleep',
data: lightSleep,
backgroundColor: this.colors.sleep.light,
stack: 'sleep'
},
{
label: 'REM Sleep',
data: remSleep,
backgroundColor: this.colors.sleep.rem,
stack: 'sleep'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'Sleep Composition by Night'
},
tooltip: {
mode: 'index',
callbacks: {
afterTitle: (items) => {
const index = items[0].dataIndex;
return `Total: ${totalSleep[index]} hours`;
}
}
}
},
scales: {
x: {
stacked: true,
title: {
display: true,
text: 'Date'
}
},
y: {
stacked: true,
title: {
display: true,
text: 'Hours'
}
}
}
}
});
// Store chart reference
this.charts.push(chart);
// Create container for trend line chart
const trendContainer = this.createChartContainer(wrapper);
const trendCanvas = document.createElement('canvas');
trendContainer.appendChild(trendCanvas);
const trendChart = new Chart(trendCanvas, {
type: 'line',
data: {
labels: dates,
datasets: [{
label: 'Sleep Duration',
data: totalSleep,
borderColor: this.colors.sleep.total,
backgroundColor: 'rgba(74, 111, 165, 0.1)',
fill: true,
tension: 0.3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'Sleep Duration Trend'
}
},
scales: {
y: {
title: {
display: true,
text: 'Hours'
},
min: Math.max(0, Math.min(...totalSleep) - 1),
max: Math.max(...totalSleep) + 1
}
}
}
});
// Store chart reference
this.charts.push(trendChart);
}
/**
* Create stress levels chart
* @param {Array} metricsData - Processed metrics data
* @param {HTMLElement} wrapper - Wrapper for the chart containers
*/
createStressChart(metricsData, wrapper) {
// Filter out days without stress data
const stressData = metricsData.filter(day =>
day.metrics['Stress Qualifier'] !== undefined
);
if (stressData.length === 0) {
this.showNoDataMessage(wrapper, 'No stress data available');
return;
}
// Prepare data for chart
const dates = stressData.map(day => day.date);
const stressQualifiers = stressData.map(day => day.metrics['Stress Qualifier']);
// Count occurrences of each stress level
const stressLevels = ['calm', 'balanced', 'stressful', 'very_stressful'];
const stressCounts = {};
stressLevels.forEach(level => {
stressCounts[level] = stressQualifiers.filter(q => q === level).length;
});
// Create container for pie chart
const pieContainer = this.createChartContainer(wrapper);
const pieCanvas = document.createElement('canvas');
pieContainer.appendChild(pieCanvas);
// Create pie chart
const pieChart = new Chart(pieCanvas, {
type: 'doughnut',
data: {
labels: ['Calm', 'Balanced', 'Stressful', 'Very Stressful'],
datasets: [{
data: [
stressCounts.calm || 0,
stressCounts.balanced || 0,
stressCounts.stressful || 0,
stressCounts.very_stressful || 0
],
backgroundColor: [
this.colors.stress.calm,
this.colors.stress.balanced,
this.colors.stress.stressful,
this.colors.stress.veryStressful
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'Stress Level Distribution'
},
legend: {
position: 'right'
}
}
}
});
// Store chart reference
this.charts.push(pieChart);
// Create container for timeline chart
const timelineContainer = this.createChartContainer(wrapper);
const timelineCanvas = document.createElement('canvas');
timelineContainer.appendChild(timelineCanvas);
// Map stress qualifiers to numeric values for the chart
const stressValues = stressQualifiers.map(qualifier => {
switch(qualifier) {
case 'calm': return 1;
case 'balanced': return 2;
case 'stressful': return 3;
case 'very_stressful': return 4;
default: return 0;
}
});
const timelineChart = new Chart(timelineCanvas, {
type: 'line',
data: {
labels: dates,
datasets: [{
label: 'Stress Level',
data: stressValues,
borderColor: this.colors.accent,
backgroundColor: 'rgba(255, 126, 103, 0.1)',
fill: true,
stepped: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'Stress Level Timeline'
},
tooltip: {
callbacks: {
label: (context) => {
const value = context.raw;
let label = '';
switch(value) {
case 1: label = 'Calm'; break;
case 2: label = 'Balanced'; break;
case 3: label = 'Stressful'; break;
case 4: label = 'Very Stressful'; break;
default: label = 'Unknown';
}
return label;
}
}
}
},
scales: {
y: {
min: 0.5,
max: 4.5,
ticks: {
callback: (value) => {
switch(value) {
case 1: return 'Calm';
case 2: return 'Balanced';
case 3: return 'Stressful';
case 4: return 'Very Stressful';
default: return '';
}
}
}
}
}
}
});
// Store chart reference
this.charts.push(timelineChart);
}
/**
* Create workout analysis chart
* @param {Array} workoutsData - Processed workouts data
* @param {HTMLElement} wrapper - Wrapper for the chart containers
*/
createWorkoutChart(workoutsData, wrapper) {
if (workoutsData.length === 0) {
this.showNoDataMessage(wrapper, 'No workout data available');
return;
}
// Prepare data for chart
const dates = workoutsData.map(workout => workout.date);
const heartRates = workoutsData.map(workout => workout.heartRate.average);
const durations = workoutsData.map(workout => workout.duration * 60); // Convert to minutes
// Create container for bar/line chart
const barContainer = this.createChartContainer(wrapper);
const canvas = document.createElement('canvas');
barContainer.appendChild(canvas);
// Create chart
const chart = new Chart(canvas, {
type: 'bar',
data: {
labels: dates,
datasets: [
{
label: 'Duration (minutes)',
data: durations,
backgroundColor: this.colors.secondary,
yAxisID: 'y',
order: 1 // Draw bars first (underneath)
},
{
label: 'Avg Heart Rate (bpm)',
data: heartRates,
borderColor: this.colors.accent,
backgroundColor: 'transparent', // No fill
borderWidth: 3, // Thicker line
pointRadius: 4, // Visible points
pointBackgroundColor: this.colors.accent,
pointBorderColor: '#fff',
pointHoverRadius: 6,
pointHoverBackgroundColor: this.colors.accent,
tension: 0.3, // Slightly curved line
type: 'line',
yAxisID: 'y1',
order: 0 // Draw line last (on top)
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: {
mode: 'index',
intersect: false
},
plugins: {
title: {
display: true,
text: 'Workout Duration and Intensity'
},
tooltip: {
callbacks: {
title: (tooltipItems) => {
return `Date: ${tooltipItems[0].label}`;
}
}
}
},
scales: {
y: {
type: 'linear',
position: 'left',
title: {
display: true,
text: 'Duration (minutes)'
},
min: 0
},
y1: {
type: 'linear',
position: 'right',
title: {
display: true,
text: 'Heart Rate (bpm)'
},
min: Math.max(0, Math.min(...heartRates) - 10),
max: Math.max(...heartRates) + 10,
grid: {
drawOnChartArea: false
}
}
}
}
});
// Store chart reference
this.charts.push(chart);
// Create workout type distribution chart if we have type data
if (workoutsData.some(workout => workout.type)) {
// Count workout types
const workoutTypes = {};
workoutsData.forEach(workout => {
const type = workout.type || 'Unknown';
workoutTypes[type] = (workoutTypes[type] || 0) + 1;
});
// Create container for pie chart
const pieContainer = this.createChartContainer(wrapper);
const pieCanvas = document.createElement('canvas');
pieContainer.appendChild(pieCanvas);
// Create color array based on number of workout types
const typeColors = Object.keys(workoutTypes).map((_, index) => {
const hue = (index * 137) % 360; // Golden angle approximation for good distribution
return `hsl(${hue}, 70%, 60%)`;
});
// Create pie chart
const pieChart = new Chart(pieCanvas, {
type: 'pie',
data: {
labels: Object.keys(workoutTypes),
datasets: [{
data: Object.values(workoutTypes),
backgroundColor: typeColors
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'Workout Type Distribution'
},
legend: {
position: 'right'
}
}
}
});
// Store chart reference
this.charts.push(pieChart);
}
}
/**
* Create HRV and recovery metrics chart
* @param {Array} metricsData - Processed metrics data
* @param {HTMLElement} wrapper - Wrapper for the chart containers
*/
createHRVChart(metricsData, wrapper) {
// Filter out days without HRV data
const hrvData = metricsData.filter(day =>
day.metrics['HRV'] !== undefined
);
if (hrvData.length === 0) {
this.showNoDataMessage(wrapper, 'No HRV data available');
return;
}
// Prepare data for chart
const dates = hrvData.map(day => day.date);
const hrvValues = hrvData.map(day => day.metrics['HRV']);
const pulseValues = hrvData.map(day => day.metrics['Pulse']);
// Create container for HRV/Pulse chart
const hrvContainer = this.createChartContainer(wrapper);
const canvas = document.createElement('canvas');
hrvContainer.appendChild(canvas);
// Create chart
const chart = new Chart(canvas, {
type: 'line',
data: {
labels: dates,
datasets: [
{
label: 'HRV',
data: hrvValues,
borderColor: this.colors.primary,
backgroundColor: 'rgba(74, 111, 165, 0.1)',
fill: true,
yAxisID: 'y'
},
{
label: 'Resting Pulse',
data: pulseValues,
borderColor: this.colors.accent,
backgroundColor: 'rgba(255, 126, 103, 0.1)',
fill: true,
yAxisID: 'y1'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'HRV and Resting Pulse'
}
},
scales: {
y: {
type: 'linear',
position: 'left',
title: {
display: true,
text: 'HRV (ms)'
}
},
y1: {
type: 'linear',
position: 'right',
title: {
display: true,
text: 'Pulse (bpm)'
},
grid: {
drawOnChartArea: false
}
}
}
}
});
// Store chart reference
this.charts.push(chart);
// Create body battery chart if data is available
const bodyBatteryData = metricsData.filter(day =>
day.metrics['Body Battery'] !== undefined
);
if (bodyBatteryData.length > 0) {
// Parse body battery values (format: "Min : X / Max : Y / Avg : Z")
const batteryDates = bodyBatteryData.map(day => day.date);
const batteryValues = bodyBatteryData.map(day => {
const batteryString = day.metrics['Body Battery'];
const avgMatch = batteryString.match(/Avg\s*:\s*(\d+)/);
return avgMatch ? parseInt(avgMatch[1]) : null;
}).filter(val => val !== null);
// Create container for body battery chart
const bbContainer = this.createChartContainer(wrapper);
const bbCanvas = document.createElement('canvas');
bbContainer.appendChild(bbCanvas);
// Create chart
const bbChart = new Chart(bbCanvas, {
type: 'line',
data: {
labels: batteryDates,
datasets: [{
label: 'Body Battery (Average)',
data: batteryValues,
borderColor: this.colors.secondary,
backgroundColor: 'rgba(111, 185, 143, 0.1)',
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
title: {
display: true,
text: 'Body Battery Trend'
}
},
scales: {
y: {
min: 0,
max: 100,
title: {
display: true,
text: 'Energy Level'
}
}
}
}
});
// Store chart reference
this.charts.push(bbChart);
}
}
/**
* Show a message when no data is available for a chart
* @param {HTMLElement} container - Container element
* @param {string} message - Message to display
*/
showNoDataMessage(container, message) {
const messageElement = document.createElement('div');
messageElement.className = 'no-data-message';
messageElement.textContent = message;
container.appendChild(messageElement);
}
/**
* Clear all charts
*/
clearCharts() {
// Destroy all chart instances
this.charts.forEach(chart => chart.destroy());
this.charts = [];
}
}
// Create a global instance
const chartsManager = new ChartsManager();
+128
View File
@@ -0,0 +1,128 @@
/**
* Health Analytics Dashboard Configuration
* Contains settings for OpenAI API, system prompts, and application configuration
*/
const CONFIG = {
// OpenAI API Configuration
openai: {
model: "gpt-4.1", // Using GPT-4.1 model
temperature: 0.7,
max_tokens: 4000,
// Note: API key should be provided by the user or stored securely
// This is a placeholder for demonstration purposes
apiKeyPlaceholder: "YOUR_OPENAI_API_KEY"
},
// System Prompts for different analysis types
systemPrompts: {
// Base system prompt that defines the AI's role and capabilities
base: `You are an expert health and fitness analyst with deep knowledge in exercise physiology,
sleep science, stress management, and recovery optimization. Your task is to analyze health metrics
and workout data to provide personalized recommendations.
You should maintain a professional, encouraging tone while providing evidence-based insights.
Focus on actionable recommendations that are realistic and tailored to the data provided.
Identify patterns, correlations, and potential areas of improvement.
Highlight both strengths and areas that need attention.
Your analysis should be structured, clear, and easy to understand for someone without medical expertise.`,
// Comprehensive analysis prompt
comprehensive: `Conduct a comprehensive analysis of all provided health and fitness data.
Examine sleep patterns, stress levels, workout performance, and recovery indicators.
Identify correlations between different metrics (e.g., how sleep quality affects workout performance).
Provide a holistic set of recommendations addressing all aspects of health and fitness.
Include short-term actions and long-term strategies for improvement.`,
// Sleep-focused analysis prompt
sleep: `Focus your analysis on sleep patterns and quality.
Evaluate total sleep duration, sleep cycle distribution (deep, light, REM), and consistency.
Identify potential sleep disruptors based on other metrics (stress, exercise timing, etc.).
Provide specific recommendations to improve sleep quality and optimize sleep cycles.
Suggest optimal sleep schedules and pre-sleep routines based on the data patterns.`,
// Stress management analysis prompt
stress: `Concentrate your analysis on stress levels and management.
Evaluate stress patterns, peak stress periods, and potential triggers.
Analyze the relationship between stress and other health metrics (sleep, recovery, etc.).
Provide targeted recommendations for stress reduction and resilience building.
Suggest specific techniques for managing high-stress periods identified in the data.`,
// Workout performance analysis prompt
workout: `Focus your analysis on workout performance and training optimization.
Evaluate workout intensity, frequency, and patterns across the data period.
Analyze performance metrics in relation to recovery indicators and sleep quality.
Provide recommendations for optimizing training schedule, intensity, and recovery.
Suggest potential adjustments to improve performance and prevent overtraining.`,
// Recovery patterns analysis prompt
recovery: `Concentrate your analysis on recovery patterns and optimization.
Evaluate body battery trends, HRV patterns, and recovery indicators.
Analyze the relationship between workouts, sleep quality, and recovery metrics.
Provide specific recommendations for enhancing recovery between workouts.
Suggest recovery strategies tailored to the individual's patterns and needs.`
},
// Recommendation style configurations
recommendationStyles: {
conservative: {
description: "Provides cautious, gradual recommendations with minimal changes to current patterns",
prompt: `Provide conservative recommendations that involve minimal changes to current routines.
Focus on small, incremental adjustments that are easy to implement.
Prioritize consistency and sustainability over dramatic changes.
Suggest gradual progression paths rather than significant overhauls.`
},
moderate: {
description: "Balanced approach with practical, evidence-based recommendations",
prompt: `Provide balanced recommendations that are practical and evidence-based.
Suggest moderate adjustments to current patterns where beneficial.
Balance ambitious goals with realistic implementation considerations.
Focus on sustainable changes that can be maintained long-term.`
},
aggressive: {
description: "Bold, ambitious recommendations for maximum results",
prompt: `Provide ambitious recommendations aimed at achieving maximum results.
Suggest significant adjustments where the data indicates potential for improvement.
Focus on optimal practices rather than minimal effective doses.
Present challenging but achievable targets based on the data patterns.`
}
},
// Report detail level configurations
reportDetailLevels: {
summary: {
description: "Brief overview with key insights and core recommendations",
prompt: `Provide a concise summary of the most important insights and recommendations.
Limit your analysis to 3-5 key observations and their implications.
Focus only on the highest-priority recommendations.
Use bullet points and brief explanations rather than detailed analysis.`
},
detailed: {
description: "Comprehensive analysis with detailed recommendations",
prompt: `Provide a detailed analysis covering all relevant aspects of the data.
Include specific observations with supporting evidence from the data.
Offer comprehensive recommendations with implementation guidance.
Explain the reasoning behind your recommendations.`
},
comprehensive: {
description: "In-depth analysis with extensive recommendations and implementation plans",
prompt: `Provide an exhaustive analysis of all data points and their interrelationships.
Include detailed observations with specific references to the data.
Offer extensive recommendations with step-by-step implementation guidance.
Explain the scientific rationale behind each recommendation.
Include potential challenges and how to overcome them.
Provide both short-term actions and long-term strategies.`
}
},
// Sample data for demonstration purposes
sampleData: {
metricsPath: "metrics.csv",
workoutsPath: "workouts.csv"
}
};
+462
View File
@@ -0,0 +1,462 @@
/**
* Data Processor Module
* Handles parsing and processing of CSV data for health metrics and workouts
*/
class DataProcessor {
constructor() {
this.metricsData = null;
this.workoutsData = null;
}
/**
* Parse CSV string into array of objects
* @param {string} csvString - Raw CSV data as string
* @returns {Array} - Array of objects with headers as keys
*/
parseCSV(csvString) {
// Split the CSV string into lines
const lines = csvString.split('\n');
// Extract headers (first line) and remove quotes and any extra characters
const headers = lines[0].split(',').map(header => {
// Clean up header - remove quotes, carriage returns, etc.
return header.replace(/^"(.*)"$/, '$1').replace(/\r$/, '');
});
// Process each data line
const results = [];
for (let i = 1; i < lines.length; i++) {
// Skip empty lines
if (!lines[i].trim()) continue;
// Split the line into values, handling quoted values with commas
const values = this.splitCSVLine(lines[i]);
// Create an object with headers as keys and values
const entry = {};
headers.forEach((header, index) => {
// Remove quotes if present and convert to appropriate type
let value = values[index] ? values[index].replace(/^"(.*)"$/, '$1').replace(/\r$/, '') : '';
// Try to convert to number if it looks like one
if (!isNaN(value) && value.trim() !== '') {
value = parseFloat(value);
}
entry[header] = value;
});
results.push(entry);
}
return results;
}
/**
* Split CSV line handling quoted values with commas
* @param {string} line - CSV line to split
* @returns {Array} - Array of values
*/
splitCSVLine(line) {
const values = [];
let inQuotes = false;
let currentValue = '';
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (char === '"') {
inQuotes = !inQuotes;
} else if (char === ',' && !inQuotes) {
values.push(currentValue);
currentValue = '';
} else {
currentValue += char;
}
}
// Add the last value
values.push(currentValue);
return values;
}
/**
* Process metrics data from CSV
* @param {string} csvString - Raw CSV data as string
* @returns {Object} - Processed metrics data
*/
processMetricsData(csvString) {
const rawData = this.parseCSV(csvString);
// Log the first few entries to verify structure
console.log('First few entries from metrics CSV:', rawData.slice(0, 5));
// Group metrics by date
const metricsByDate = {};
rawData.forEach(entry => {
// Extract date from timestamp (format: "2025-04-14 00:00:00")
const date = entry.Timestamp.split(' ')[0];
if (!metricsByDate[date]) {
metricsByDate[date] = {};
}
// Add metric to the date group
// Ensure Type is trimmed to handle any extra spaces
const metricType = entry.Type.trim();
// Get the value from the correct column (might be '"Value"' instead of 'Value')
let metricValue = null;
if (entry.Value !== undefined) {
metricValue = entry.Value;
} else if (entry['"Value"'] !== undefined) {
metricValue = entry['"Value"'];
}
// Convert Value to number if it's numeric
if (metricValue !== null && !isNaN(metricValue) && metricValue.toString().trim() !== '') {
metricValue = parseFloat(metricValue);
}
metricsByDate[date][metricType] = metricValue;
});
// Log the metrics by date to verify structure
console.log('Sample of metrics by date:', Object.keys(metricsByDate).slice(0, 3).map(date => ({
date,
metrics: metricsByDate[date]
})));
// Convert to array and sort by date
const processedData = Object.keys(metricsByDate).map(date => {
return {
date,
metrics: metricsByDate[date]
};
}).sort((a, b) => new Date(a.date) - new Date(b.date));
this.metricsData = processedData;
return processedData;
}
/**
* Process workouts data from CSV
* @param {string} csvString - Raw CSV data as string
* @returns {Array} - Processed workouts data
*/
processWorkoutsData(csvString) {
const rawData = this.parseCSV(csvString);
// Process each workout entry
const processedData = rawData.map(workout => {
// Convert workout day to date object for easier comparison
const workoutDate = new Date(workout.WorkoutDay);
return {
date: workout.WorkoutDay,
type: workout.WorkoutType,
title: workout.Title,
distance: workout.DistanceInMeters,
duration: workout.TimeTotalInHours,
heartRate: {
average: workout.HeartRateAverage,
max: workout.HeartRateMax
},
power: {
average: workout.PowerAverage,
max: workout.PowerMax
},
energy: workout.Energy,
zones: {
heartRate: this.extractHeartRateZones(workout),
power: this.extractPowerZones(workout)
},
rpe: workout.Rpe,
feeling: workout.Feeling,
cadence: {
average: workout.CadenceAverage,
max: workout.CadenceMax
},
velocity: {
average: workout.VelocityAverage,
max: workout.VelocityMax
},
comments: {
athlete: workout.AthleteComments,
coach: workout.CoachComments
}
};
}).sort((a, b) => new Date(a.date) - new Date(b.date));
this.workoutsData = processedData;
return processedData;
}
/**
* Extract heart rate zone data from workout entry
* @param {Object} workout - Workout data entry
* @returns {Array} - Heart rate zone minutes
*/
extractHeartRateZones(workout) {
const zones = [];
for (let i = 1; i <= 10; i++) {
const zoneKey = `HRZone${i}Minutes`;
if (workout[zoneKey] !== undefined && workout[zoneKey] !== '') {
zones.push({
zone: i,
minutes: parseFloat(workout[zoneKey])
});
}
}
return zones;
}
/**
* Extract power zone data from workout entry
* @param {Object} workout - Workout data entry
* @returns {Array} - Power zone minutes
*/
extractPowerZones(workout) {
const zones = [];
for (let i = 1; i <= 10; i++) {
const zoneKey = `PWRZone${i}Minutes`;
if (workout[zoneKey] !== undefined && workout[zoneKey] !== '') {
zones.push({
zone: i,
minutes: parseFloat(workout[zoneKey])
});
}
}
return zones;
}
/**
* Load sample data from files
* @returns {Promise} - Promise resolving when data is loaded
*/
async loadSampleData() {
try {
// Load metrics data using XMLHttpRequest to avoid CORS issues with file:// URLs
const metricsCSV = await this.loadFileWithXHR(CONFIG.sampleData.metricsPath);
this.processMetricsData(metricsCSV);
// Load workouts data
const workoutsCSV = await this.loadFileWithXHR(CONFIG.sampleData.workoutsPath);
this.processWorkoutsData(workoutsCSV);
return {
metrics: this.metricsData,
workouts: this.workoutsData
};
} catch (error) {
console.error('Error loading sample data:', error);
throw error;
}
}
/**
* Load a file using XMLHttpRequest (works with file:// URLs)
* @param {string} filePath - Path to the file
* @returns {Promise<string>} - Promise resolving with file contents
*/
loadFileWithXHR(filePath) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', filePath, true);
xhr.responseType = 'text';
xhr.onload = function() {
if (xhr.status === 200 || (xhr.status === 0 && xhr.responseText)) {
resolve(xhr.responseText);
} else {
reject(new Error(`Failed to load file: ${filePath}`));
}
};
xhr.onerror = function() {
reject(new Error(`Network error loading file: ${filePath}`));
};
xhr.send();
});
}
/**
* Process file data from file inputs
* @param {File} metricsFile - Metrics CSV file
* @param {File} workoutsFile - Workouts CSV file
* @returns {Promise} - Promise resolving with processed data
*/
async processFiles(metricsFile, workoutsFile) {
try {
// Process metrics file
const metricsData = await this.readFile(metricsFile);
this.processMetricsData(metricsData);
// Process workouts file
const workoutsData = await this.readFile(workoutsFile);
this.processWorkoutsData(workoutsData);
return {
metrics: this.metricsData,
workouts: this.workoutsData
};
} catch (error) {
console.error('Error processing files:', error);
throw error;
}
}
/**
* Read file as text
* @param {File} file - File to read
* @returns {Promise} - Promise resolving with file contents
*/
readFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (event) => {
resolve(event.target.result);
};
reader.onerror = (error) => {
reject(error);
};
reader.readAsText(file);
});
}
/**
* Prepare data for OpenAI analysis
* @returns {Object} - Data formatted for analysis
*/
prepareDataForAnalysis() {
if (!this.metricsData || !this.workoutsData) {
throw new Error('Data not loaded. Please load data first.');
}
return {
metrics: this.metricsData,
workouts: this.workoutsData,
summary: this.generateDataSummary()
};
}
/**
* Generate summary statistics from the data
* @returns {Object} - Summary statistics
*/
generateDataSummary() {
if (!this.metricsData || !this.workoutsData) {
return null;
}
// Log all available metric types from the first few days to help diagnose issues
console.log('Available metric types in first few days:');
this.metricsData.slice(0, 3).forEach(day => {
console.log(`Day ${day.date} metrics:`, Object.keys(day.metrics));
});
// Find sleep hours data with case-insensitive matching
const sleepData = [];
const sleepHoursKey = 'Sleep Hours'; // The expected key
this.metricsData.forEach(day => {
// Try to find the sleep hours key (case-insensitive)
let foundSleepHours = false;
// First try the exact key
if (day.metrics[sleepHoursKey] !== undefined) {
sleepData.push(parseFloat(day.metrics[sleepHoursKey]));
foundSleepHours = true;
} else {
// If not found, try case-insensitive search
for (const key in day.metrics) {
if (key.toLowerCase() === sleepHoursKey.toLowerCase()) {
sleepData.push(parseFloat(day.metrics[key]));
foundSleepHours = true;
console.log(`Found sleep hours with different case: "${key}"`);
break;
}
}
}
if (!foundSleepHours) {
console.log(`No sleep hours found for day ${day.date}`);
}
});
console.log(`Found ${sleepData.length} days with sleep data out of ${this.metricsData.length} total days`);
const sleepStats = sleepData.length > 0 ? {
average: this.calculateAverage(sleepData),
min: Math.min(...sleepData),
max: Math.max(...sleepData)
} : null;
console.log('Sleep stats:', sleepStats);
// Calculate stress statistics
const stressQualifiers = this.metricsData
.filter(day => day.metrics['Stress Qualifier'] !== undefined)
.map(day => day.metrics['Stress Qualifier']);
const stressQualifierCounts = {};
stressQualifiers.forEach(qualifier => {
stressQualifierCounts[qualifier] = (stressQualifierCounts[qualifier] || 0) + 1;
});
// Calculate workout statistics
const workoutDistances = this.workoutsData
.map(workout => parseFloat(workout.distance));
const workoutStats = workoutDistances.length > 0 ? {
count: this.workoutsData.length,
totalDistance: this.calculateSum(workoutDistances),
averageDistance: this.calculateAverage(workoutDistances),
averageHeartRate: this.calculateAverage(
this.workoutsData.map(w => parseFloat(w.heartRate.average))
)
} : null;
return {
dateRange: {
start: this.metricsData[0]?.date,
end: this.metricsData[this.metricsData.length - 1]?.date
},
sleep: sleepStats,
stress: {
qualifiers: stressQualifierCounts
},
workouts: workoutStats
};
}
/**
* Calculate average of array values
* @param {Array} values - Array of numbers
* @returns {number} - Average value
*/
calculateAverage(values) {
if (!values || values.length === 0) return 0;
return values.reduce((sum, val) => sum + val, 0) / values.length;
}
/**
* Calculate sum of array values
* @param {Array} values - Array of numbers
* @returns {number} - Sum of values
*/
calculateSum(values) {
if (!values || values.length === 0) return 0;
return values.reduce((sum, val) => sum + val, 0);
}
}
// Create a global instance
const dataProcessor = new DataProcessor();
+232
View File
@@ -0,0 +1,232 @@
/**
* OpenAI Service Module
* Handles interactions with the OpenAI API for generating health recommendations
*/
class OpenAIService {
constructor() {
this.apiKey = null;
this.model = CONFIG.openai.model;
this.temperature = CONFIG.openai.temperature;
this.maxTokens = CONFIG.openai.max_tokens;
}
/**
* Set the API key for OpenAI requests
* @param {string} apiKey - OpenAI API key
*/
setApiKey(apiKey) {
this.apiKey = apiKey;
}
/**
* Check if API key is set
* @returns {boolean} - Whether API key is set
*/
hasApiKey() {
return !!this.apiKey;
}
/**
* Generate a system prompt based on user configuration
* @param {Object} options - Analysis options
* @returns {string} - Complete system prompt
*/
generateSystemPrompt(options) {
// Start with the base prompt
let systemPrompt = CONFIG.systemPrompts.base;
// Add analysis focus prompt
if (options.analysisFocus && CONFIG.systemPrompts[options.analysisFocus]) {
systemPrompt += '\n\n' + CONFIG.systemPrompts[options.analysisFocus];
}
// Add recommendation style prompt
if (options.recommendationStyle &&
CONFIG.recommendationStyles[options.recommendationStyle]) {
systemPrompt += '\n\n' + CONFIG.recommendationStyles[options.recommendationStyle].prompt;
}
// Add report detail level prompt
if (options.reportDetail &&
CONFIG.reportDetailLevels[options.reportDetail]) {
systemPrompt += '\n\n' + CONFIG.reportDetailLevels[options.reportDetail].prompt;
}
return systemPrompt;
}
/**
* Generate a user prompt with data and custom instructions
* @param {Object} data - Processed health and workout data
* @param {string} customPrompt - User's custom instructions
* @returns {string} - Complete user prompt
*/
generateUserPrompt(data, customPrompt) {
// Create a structured representation of the data
const dataString = JSON.stringify(data, null, 2);
let userPrompt = `Please analyze the following health metrics and workout data and provide personalized recommendations:
DATA:
${dataString}`;
// Add custom instructions if provided
if (customPrompt && customPrompt.trim()) {
userPrompt += `\n\nADDITIONAL INSTRUCTIONS:
${customPrompt}`;
}
userPrompt += `\n\nPlease format your response in HTML for direct display in a web application. Use h2, h3, p, ul, li, and other appropriate HTML tags to structure your response. You may use <strong>, <em>, and other formatting tags as needed. Do not include <!DOCTYPE>, <html>, <head>, or <body> tags.
Note: Data visualizations will be automatically generated for sleep patterns, stress levels, workout performance, and recovery metrics, so you don't need to describe these visually in your response. Focus on providing insights and recommendations based on the data.`;
return userPrompt;
}
/**
* Generate health recommendations using OpenAI API
* @param {Object} data - Processed health and workout data
* @param {Object} options - Analysis options
* @returns {Promise} - Promise resolving with the generated recommendations and raw input data
*/
async generateRecommendations(data, options) {
if (!this.hasApiKey()) {
throw new Error('OpenAI API key not set. Please set an API key before generating recommendations.');
}
try {
const systemPrompt = this.generateSystemPrompt(options);
const userPrompt = this.generateUserPrompt(data, options.customPrompt);
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`
},
body: JSON.stringify({
model: this.model,
messages: [
{
role: 'system',
content: systemPrompt
},
{
role: 'user',
content: userPrompt
}
],
temperature: this.temperature,
max_tokens: this.maxTokens
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`OpenAI API Error: ${errorData.error?.message || 'Unknown error'}`);
}
const result = await response.json();
return {
content: result.choices[0].message.content,
rawInput: {
systemPrompt,
userPrompt
}
};
} catch (error) {
console.error('Error generating recommendations:', error);
throw error;
}
}
/**
* Generate a demo response without using the API
* @param {Object} data - Processed health and workout data
* @param {Object} options - Analysis options
* @returns {Promise} - Promise resolving with the generated demo recommendations and raw input data
*/
async generateDemoRecommendations(data, options) {
// This function provides a demo response when no API key is available
// It returns a pre-written HTML response based on the data and options
// Generate the prompts that would be sent to the API
const systemPrompt = this.generateSystemPrompt(options);
const userPrompt = this.generateUserPrompt(data, options.customPrompt);
// Extract some basic stats for the demo
const sleepAvg = data.summary.sleep?.average.toFixed(1) || 'N/A';
const workoutCount = data.summary.workouts?.count || 0;
const stressLevels = Object.keys(data.summary.stress?.qualifiers || {}).join(', ');
const dateRange = `${data.summary.dateRange?.start || 'N/A'} to ${data.summary.dateRange?.end || 'N/A'}`;
// Create a demo HTML response
const demoHTML = `
<div class="report-section">
<h2>Health & Fitness Analysis</h2>
<p class="report-date">Analysis Period: ${dateRange}</p>
<div class="report-summary">
<h3>Summary</h3>
<p>This analysis is based on ${data.metrics.length} days of health metrics and ${workoutCount} workouts. The data shows an average sleep duration of ${sleepAvg} hours per night with stress levels categorized as: ${stressLevels}.</p>
<p><strong>Note:</strong> This is a demo analysis. For a complete analysis, please provide an OpenAI API key.</p>
</div>
<div class="report-section">
<h3>Sleep Analysis</h3>
<p>Your average sleep duration of ${sleepAvg} hours is ${sleepAvg >= 7 ? 'within the recommended range' : 'below the recommended range'} for optimal health. Sleep quality indicators suggest ${sleepAvg >= 7 ? 'adequate' : 'potential issues with'} recovery between activities.</p>
<ul>
<li><strong>Recommendation:</strong> ${sleepAvg >= 7 ? 'Maintain your current sleep schedule' : 'Aim to increase sleep duration by 30-60 minutes'} to optimize recovery and performance.</li>
<li><strong>Recommendation:</strong> Consider implementing a consistent pre-sleep routine to improve sleep quality.</li>
</ul>
</div>
<div class="report-section">
<h3>Stress Management</h3>
<p>Your stress levels show ${stressLevels.includes('balanced') ? 'a good balance overall' : 'potential areas for improvement'}, with some periods of elevated stress detected.</p>
<ul>
<li><strong>Recommendation:</strong> Incorporate daily mindfulness practices (5-10 minutes) to manage stress levels.</li>
<li><strong>Recommendation:</strong> Consider scheduling regular recovery days, especially following high-stress periods.</li>
</ul>
</div>
<div class="report-section">
<h3>Workout Analysis</h3>
<p>Your training data shows ${workoutCount} workouts during the analysis period, with consistent effort levels.</p>
<ul>
<li><strong>Recommendation:</strong> ${workoutCount >= 3 ? 'Maintain your current workout frequency' : 'Consider increasing workout frequency to 3-4 sessions per week'} for optimal fitness development.</li>
<li><strong>Recommendation:</strong> Incorporate varied intensity levels in your training to stimulate different energy systems.</li>
</ul>
</div>
<div class="report-section">
<h3>Next Steps</h3>
<p>Based on this analysis, focus on the following areas:</p>
<ol>
<li>${sleepAvg >= 7 ? 'Maintain your sleep consistency' : 'Improve sleep duration and quality'}</li>
<li>Implement regular stress management practices</li>
<li>${workoutCount >= 3 ? 'Continue your consistent training schedule' : 'Increase workout frequency gradually'}</li>
<li>Monitor your progress and adjust based on recovery indicators</li>
</ol>
</div>
</div>
`;
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 1500));
return {
content: demoHTML,
rawInput: {
systemPrompt,
userPrompt
}
};
}
}
// Create a global instance
const openaiService = new OpenAIService();
+236
View File
@@ -0,0 +1,236 @@
/**
* Report Generator Module
* Handles the generation and display of health reports
*/
class ReportGenerator {
constructor() {
this.reportContainer = document.getElementById('report-container');
this.loadingIndicator = document.querySelector('.loading-indicator');
this.reportSection = document.querySelector('.report-section');
}
/**
* Generate a health report based on data and options
* @param {Object} data - Processed health and workout data
* @param {Object} options - Analysis options
* @returns {Promise} - Promise resolving when report is generated
*/
async generateReport(data, options) {
try {
// Show loading indicator
this.showLoading();
let result;
// Check if we have an API key
if (options.apiKey && options.apiKey.trim()) {
// Set the API key
openaiService.setApiKey(options.apiKey);
// Generate recommendations using the OpenAI API
result = await openaiService.generateRecommendations(data, options);
} else {
// Generate demo recommendations without API
result = await openaiService.generateDemoRecommendations(data, options);
}
// Display the report
this.displayReport(result.content, options.showGptInput ? result.rawInput : null);
return result.content;
} catch (error) {
this.displayError(error.message);
throw error;
} finally {
// Hide loading indicator
this.hideLoading();
}
}
/**
* Display the generated report
* @param {string} reportContent - HTML content of the report
* @param {Object|null} rawInputData - Raw input data sent to GPT-4.1
*/
displayReport(reportContent, rawInputData = null) {
// Set the report content
this.reportContainer.innerHTML = reportContent;
// Create charts section
const chartsSection = document.createElement('div');
chartsSection.className = 'charts-section';
const chartsSectionTitle = document.createElement('h2');
chartsSectionTitle.textContent = 'Data Visualizations';
chartsSection.appendChild(chartsSectionTitle);
const chartsSectionDescription = document.createElement('p');
chartsSectionDescription.textContent = 'Interactive visualizations of your health and fitness data.';
chartsSection.appendChild(chartsSectionDescription);
// Add charts section to the report
this.reportContainer.appendChild(chartsSection);
// Generate charts based on the data
if (dataProcessor.metricsData && dataProcessor.workoutsData) {
const data = {
metrics: dataProcessor.metricsData,
workouts: dataProcessor.workoutsData
};
// Create charts
chartsManager.createCharts(data, chartsSection);
}
// Add raw input data section if available
if (rawInputData) {
const rawDataSection = document.createElement('div');
rawDataSection.className = 'raw-data-section';
const rawDataToggle = document.createElement('div');
rawDataToggle.className = 'raw-data-toggle';
rawDataToggle.innerHTML = `
<h3>GPT-4.1 Input Data</h3>
<span class="toggle-icon">▼</span>
`;
const rawDataContainer = document.createElement('div');
rawDataContainer.className = 'raw-data-container';
const systemPromptPre = document.createElement('pre');
systemPromptPre.textContent = `/* System Prompt */\n${rawInputData.systemPrompt}`;
const userPromptPre = document.createElement('pre');
userPromptPre.textContent = `\n\n/* User Prompt */\n${rawInputData.userPrompt}`;
rawDataContainer.appendChild(systemPromptPre);
rawDataContainer.appendChild(userPromptPre);
rawDataSection.appendChild(rawDataToggle);
rawDataSection.appendChild(rawDataContainer);
this.reportContainer.appendChild(rawDataSection);
// Add toggle functionality
rawDataToggle.addEventListener('click', () => {
rawDataContainer.classList.toggle('hidden');
rawDataToggle.querySelector('.toggle-icon').textContent =
rawDataContainer.classList.contains('hidden') ? '▶' : '▼';
});
}
// Show the report section
this.reportSection.classList.remove('hidden');
// Scroll to the report section
this.reportSection.scrollIntoView({ behavior: 'smooth' });
}
/**
* Display an error message
* @param {string} errorMessage - Error message to display
*/
displayError(errorMessage) {
const errorHTML = `
<div class="error-message">
<h3>Error Generating Report</h3>
<p>${errorMessage}</p>
<p>Please try again or check your settings.</p>
</div>
`;
this.reportContainer.innerHTML = errorHTML;
this.reportSection.classList.remove('hidden');
}
/**
* Show the loading indicator
*/
showLoading() {
this.loadingIndicator.classList.remove('hidden');
this.reportContainer.innerHTML = '';
this.reportSection.classList.remove('hidden');
}
/**
* Hide the loading indicator
*/
hideLoading() {
this.loadingIndicator.classList.add('hidden');
}
/**
* Generate a PDF from the report
* @returns {Promise} - Promise resolving when PDF is generated
*/
async generatePDF() {
try {
// Show a loading indicator or message
const downloadButton = document.getElementById('download-report');
const originalText = downloadButton.innerHTML;
downloadButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Generating PDF...';
downloadButton.disabled = true;
// Add PDF-specific class to constrain chart widths
this.reportContainer.classList.add('pdf-export');
// Wait a moment to ensure all charts are fully rendered with new styles
await new Promise(resolve => setTimeout(resolve, 800));
// Configure PDF options
const element = this.reportContainer;
const opt = {
margin: [10, 10],
filename: 'health-analytics-report.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: {
scale: 2,
useCORS: true,
logging: false
},
jsPDF: {
unit: 'mm',
format: 'a4',
orientation: 'portrait'
}
};
// Generate and save the PDF
await html2pdf().set(opt).from(element).save();
// Remove PDF-specific class
this.reportContainer.classList.remove('pdf-export');
// Restore button state
downloadButton.innerHTML = originalText;
downloadButton.disabled = false;
return true;
} catch (error) {
console.error('Error generating PDF:', error);
alert('Error generating PDF: ' + error.message);
// Remove PDF-specific class in case of error
this.reportContainer.classList.remove('pdf-export');
// Restore button state
const downloadButton = document.getElementById('download-report');
downloadButton.innerHTML = '<i class="fas fa-download"></i> Download PDF';
downloadButton.disabled = false;
return false;
}
}
/**
* Reset the report view
*/
resetReport() {
this.reportContainer.innerHTML = '';
this.reportSection.classList.add('hidden');
}
}
// Create a global instance
const reportGenerator = new ReportGenerator();