/** * 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(); });