Files
sport-science-coach/js/app.js
T
2025-04-27 17:43:41 +02:00

348 lines
12 KiB
JavaScript

/**
* 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');
this.sleepGoalInput = document.getElementById('sleep-goal-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');
// Initialize sleep goal input from localStorage
const storedGoal = localStorage.getItem('sleep_goal');
if (storedGoal !== null) {
this.sleepGoalInput.value = storedGoal;
}
}
/**
* 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));
// Sleep goal input event
this.sleepGoalInput.addEventListener('input', (e) => {
const value = e.target.value;
if (value && !isNaN(value)) {
localStorage.setItem('sleep_goal', value);
} else {
localStorage.removeItem('sleep_goal');
}
});
// 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();
// Pass sleep goal to chartsManager
const sleepGoalValue = parseFloat(this.sleepGoalInput.value);
if (!isNaN(sleepGoalValue) && sleepGoalValue > 0) {
chartsManager.sleepGoal = sleepGoalValue;
} else {
chartsManager.sleepGoal = null;
}
// 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();
});