348 lines
12 KiB
JavaScript
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();
|
|
});
|