document.addEventListener('DOMContentLoaded', () => { const form = document.getElementById("predictionForm"); const resultContainer = document.getElementById("result-container"); if (resultContainer) { resultContainer.style.display = 'none'; } // Validation helper const validateField = (input) => { const value = input.value.trim(); const min = parseFloat(input.min); const max = parseFloat(input.max); if (!value) return false; if (input.type === 'number') { const numValue = parseFloat(value); return !isNaN(numValue) && numValue >= min && numValue <= max; } return true; }; // Add validation feedback const addValidationFeedback = (input) => { const validateInput = () => { const formGroup = input.closest('.form-group'); const value = input.value.trim(); const isValid = value !== '' && validateField(input); formGroup.classList.remove('error', 'success'); formGroup.classList.add(isValid ? 'success' : 'error'); const existingMessage = formGroup.querySelector('.validation-message'); if (existingMessage) existingMessage.remove(); const message = document.createElement('div'); message.className = `validation-message ${isValid ? 'success' : 'error'}`; message.textContent = value === '' ? 'This field is required' : (isValid ? 'Valid input' : 'Please check the value'); formGroup.appendChild(message); return isValid; }; input.addEventListener('input', validateInput); input.addEventListener('blur', validateInput); }; document.querySelectorAll('input, select').forEach(addValidationFeedback); // Animate counter function animateCounter(element, target) { let start = 0; const duration = 1000; const stepTime = Math.abs(Math.floor(duration / target)); const timer = setInterval(() => { start++; element.textContent = start + '%'; if (start >= target) clearInterval(timer); }, stepTime); } // Update gauge SVG function updateGauge(elementId, percent, textId, tooltipId, tooltipMessages) { const gauge = document.querySelector(elementId); const text = document.querySelector(textId); const tooltip = document.querySelector(tooltipId); const radius = elementId.includes('radial') ? 40 : 54; const circumference = 2 * Math.PI * radius; const offset = circumference - (percent / 100) * circumference; gauge.style.strokeDasharray = `${circumference} ${circumference}`; gauge.style.strokeDashoffset = offset; if (text) text.textContent = `${Math.round(percent)}%`; if (tooltip && tooltipMessages) { if (percent >= 75) { tooltip.textContent = tooltipMessages.high; } else if (percent >= 50) { tooltip.textContent = tooltipMessages.medium; } else { tooltip.textContent = tooltipMessages.low; } } } // Mapping of recommendation keywords to professional resources const resourceLinks = { 'training': { url: 'https://www.acsm.org/docs/default-source/files-for-resource-library/acsms-guidelines-for-exercise-testing-and-prescription-(10th-ed.).pdf', source: 'American College of Sports Medicine' }, 'intensity': { url: 'https://www.mayoclinic.org/healthy-lifestyle/fitness/in-depth/exercise-intensity/art-20046887', source: 'Mayo Clinic' }, 'recovery': { url: 'https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5592286/', source: 'PubMed - Sports Medicine Journal' }, 'rest': { url: 'https://www.sleepfoundation.org/physical-activity/athletic-performance-and-sleep', source: 'Sleep Foundation' }, 'injury': { url: 'https://orthoinfo.aaos.org/en/diseases--conditions/sports-injuries/', source: 'American Academy of Orthopaedic Surgeons' }, 'prevention': { url: 'https://www.nata.org/professional-interests/safe-sports-environments/injury-prevention', source: 'National Athletic Trainers\' Association' } }; // Enhanced recommendation processing function enhanceRecommendations(recommendations, factorData, featureImportance) { const categories = { 'Training Adjustments': [], 'Recovery Strategies': [], 'Injury Prevention': [] }; recommendations.forEach((rec, index) => { let category = 'Injury Prevention'; if (rec.toLowerCase().includes('training') || rec.toLowerCase().includes('intensity')) { category = 'Training Adjustments'; } else if (rec.toLowerCase().includes('recovery') || rec.toLowerCase().includes('rest')) { category = 'Recovery Strategies'; } const priority = calculatePriority(rec, factorData, featureImportance); // Find the most relevant resource link let resource = { url: 'https://www.healthline.com/health/sports-injuries', source: 'Healthline' }; for (const [keyword, link] of Object.entries(resourceLinks)) { if (rec.toLowerCase().includes(keyword)) { resource = link; break; } } categories[category].push({ id: `rec-${index}`, text: rec, priority: priority, details: generateDetails(rec, factorData), completed: false, resourceUrl: resource.url, resourceSource: resource.source }); }); Object.keys(categories).forEach(category => { categories[category].sort((a, b) => b.priority - a.priority); }); return categories; } function calculatePriority(recommendation, factorData, featureImportance) { let priority = 0.5; const keywords = { 'Training_Load_Score': ['training', 'intensity'], 'Fatigue_Level': ['fatigue', 'rest'], 'Recovery_Time_Between_Sessions': ['recovery', 'rest'], 'Previous_Injury_Count': ['injury', 'prevention'] }; Object.keys(keywords).forEach(key => { if (keywords[key].some(kw => recommendation.toLowerCase().includes(kw))) { const impact = (factorData[key] || 0) * (featureImportance[key] || 0); priority = Math.max(priority, impact); } }); return Math.min(1, Math.max(0, priority)); } function generateDetails(recommendation, factorData) { if (recommendation.toLowerCase().includes('training') && factorData.High_Intensity_Training_Hours > 10) { return `Reduce high-intensity sessions by 1-2 hours/week to lower training load.`; } else if (recommendation.toLowerCase().includes('recovery') && factorData.Recovery_Time_Between_Sessions < 12) { return `Increase rest periods to at least 12 hours between sessions.`; } else if (recommendation.toLowerCase().includes('injury') && factorData.Previous_Injury_Count > 2) { return `Consult a physiotherapist to address recurring injury risks.`; } return `Follow this recommendation consistently for optimal results.`; } function loadCompletedRecommendations() { const stored = localStorage.getItem('completedRecommendations'); return stored ? JSON.parse(stored) : {}; } function saveCompletedRecommendations(completed) { localStorage.setItem('completedRecommendations', JSON.stringify(completed)); } if (form) { form.addEventListener("submit", async (e) => { e.preventDefault(); document.querySelectorAll('.validation-message').forEach(msg => msg.remove()); document.querySelectorAll('.form-group').forEach(group => group.classList.remove('error')); const inputs = form.querySelectorAll('input, select'); let isValid = true; inputs.forEach(input => { const value = input.value.trim(); const formGroup = input.closest('.form-group'); if (value === '') { isValid = false; formGroup.classList.add('error'); const message = document.createElement('div'); message.className = 'validation-message error'; message.innerHTML = `Required: Please fill out this field`; formGroup.appendChild(message); input.classList.add('shake'); setTimeout(() => input.classList.remove('shake'), 500); input.style.borderColor = '#ef4444'; input.style.boxShadow = '0 0 10px rgba(239, 68, 68, 0.3)'; } }); if (!isValid) { const firstError = form.querySelector('.form-group.error'); if (firstError) { firstError.scrollIntoView({ behavior: 'smooth', block: 'center' }); } return; } const rawData = Object.fromEntries(new FormData(form).entries()); const formData = new FormData(form); const data = Object.fromEntries(formData.entries()); const genderMap = { "Male": 0, "Female": 1 }; const experienceMap = { "Beginner": 0, "Intermediate": 1, "Advanced": 2, "Professional": 3 }; const injuryTypeMap = { "None": 0, "Sprain": 1, "Ligament Tear": 2, "Tendonitis": 3, "Strain": 4, "Fracture": 5 }; const sportTypeMap = { "Football": 0, "Basketball": 1, "Swimming": 2, "Running": 3, "Tennis": 4, "Volleyball": 5 }; data.Gender = genderMap[data.Gender] ?? 0; data.Experience_Level = experienceMap[data.Experience_Level] ?? 0; data.Previous_Injury_Type = injuryTypeMap[data.Previous_Injury_Type] ?? 0; data.Sport_Type = sportTypeMap[data.Sport_Type] ?? 0; Object.keys(data).forEach(key => { data[key] = isNaN(data[key]) ? 0 : parseFloat(data[key]); }); const outlierWarnings = []; if (data.Age < 18 && data.Total_Weekly_Training_Hours > 40) { outlierWarnings.push('Unusual training hours for age under 18. Consider reducing to prevent burnout.'); } if (data.Sprint_Speed > 10 && data.Age > 50) { outlierWarnings.push('Sprint speed seems high for age over 50. Please verify this value.'); } if (data.Fatigue_Level > 8 && data.Recovery_Time_Between_Sessions < 12) { outlierWarnings.push('High fatigue with low recovery time may skew results. Consider adjusting inputs.'); } if (outlierWarnings.length > 0) { const warningDiv = document.createElement('div'); warningDiv.style.color = '#facc15'; warningDiv.style.marginBottom = '20px'; warningDiv.innerHTML = `Warning:`; form.appendChild(warningDiv); } try { console.log('Sending prediction request to server:', data); const response = await fetch("https://amrgaberr-injury-prediction-system.hf.space/predict", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (!response.ok) throw new Error("Prediction failed"); const result = await response.json(); console.log('Received prediction result:', result); const { predicted_risk_level, injury_likelihood_percent, model_class_probability, recommendations, feature_importance } = result; if (!predicted_risk_level || typeof injury_likelihood_percent !== 'number' || !Array.isArray(recommendations)) { throw new Error("Invalid API response format"); } const trainingLoad = Math.min(100, Math.max(0, (parseFloat(data.Training_Load_Score) / 100) * 100)); const recoveryStatus = Math.min(100, Math.max(0, (parseFloat(data.Recovery_Time_Between_Sessions) / 48) * 100)); const injuryImpact = Math.min(100, Math.max(0, (parseFloat(data.Previous_Injury_Count) / 10) * 100)); const recoveryPerTraining = data.Total_Weekly_Training_Hours > 0 ? Math.min(100, Math.max(0, (parseFloat(data.Recovery_Time_Between_Sessions) / parseFloat(data.Total_Weekly_Training_Hours)) * 100)) : 0; const totalWeeklyHours = Math.min(100, Math.max(0, (parseFloat(data.Total_Weekly_Training_Hours) / 50) * 100)); const fatigueLevel = Math.min(100, Math.max(0, (parseFloat(data.Fatigue_Level) / 10) * 100)); const highIntensityHours = Math.min(100, Math.max(0, (parseFloat(data.High_Intensity_Training_Hours) / 20) * 100)); const experienceLevel = Math.min(100, Math.max(0, (data.Experience_Level / 3) * 100)); const defaultFeatureImportance = { Training_Load_Score: 0.22, Recovery_Per_Training: 0.18, Total_Weekly_Training_Hours: 0.16, Fatigue_Level: 0.14, Recovery_Time_Between_Sessions: 0.12, High_Intensity_Training_Hours: 0.10 }; const finalFeatureImportance = feature_importance || defaultFeatureImportance; const enhancedRecommendations = enhanceRecommendations(recommendations, { Training_Load_Score: trainingLoad, Recovery_Per_Training: recoveryPerTraining, Total_Weekly_Training_Hours: totalWeeklyHours, Fatigue_Level: fatigueLevel, Recovery_Time_Between_Sessions: recoveryStatus, High_Intensity_Training_Hours: highIntensityHours, Previous_Injury_Count: injuryImpact }, finalFeatureImportance); const fullReportData = { predicted_risk_level, injury_likelihood_percent, model_class_probability, recommendations: enhancedRecommendations, feature_importance: finalFeatureImportance, factorData: { Training_Load_Score: trainingLoad, Recovery_Per_Training: recoveryPerTraining, Total_Weekly_Training_Hours: totalWeeklyHours, Fatigue_Level: fatigueLevel, Recovery_Time_Between_Sessions: recoveryStatus, High_Intensity_Training_Hours: highIntensityHours, Experience_Level: experienceLevel, Intensity_Ratio: data.High_Intensity_Training_Hours > 0 ? Math.min(100, Math.max(0, (parseFloat(data.High_Intensity_Training_Hours) / parseFloat(data.Total_Weekly_Training_Hours)) * 100)) : 0, Endurance_Score: Math.min(100, Math.max(0, (parseFloat(data.Endurance_Score) / 10) * 100)), Sprint_Speed: Math.min(100, Math.max(0, (parseFloat(data.Sprint_Speed) / 20) * 100)), Agility_Score: Math.min(100, Math.max(0, (parseFloat(data.Agility_Score) / 10) * 100)), Flexibility_Score: Math.min(100, Math.max(0, (parseFloat(data.Flexibility_Score) / 10) * 100)), Age: Math.min(100, Math.max(0, (parseFloat(data.Age) / 80) * 100)), Strength_Training_Frequency: Math.min(100, Math.max(0, (parseFloat(data.Strength_Training_Frequency) / 7) * 100)), Sport_Type: Math.min(100, Math.max(0, data.Sport_Type * 20)), Gender: data.Gender * 100, Previous_Injury_Count: injuryImpact, Previous_Injury_Type: Math.min(100, Math.max(0, data.Previous_Injury_Type * 20)) }, profile: { age: rawData.Age, gender: rawData.Gender, sport: rawData.Sport_Type }, timestamp: new Date().toISOString(), version: '1.1.0' }; console.log('Storing prediction data in localStorage:', fullReportData); localStorage.setItem('predictionData', JSON.stringify(fullReportData)); document.getElementById('risk-level').textContent = predicted_risk_level; document.getElementById('likelihood-percent').textContent = `${Math.round(injury_likelihood_percent)}%`; updateGauge('.summary-gauge .gauge-value', injury_likelihood_percent, '.summary-gauge .gauge-text', null, null); updateGauge('#training-load-gauge', trainingLoad, '#training-load-text', '#training-load-tooltip', { high: 'High training load may significantly increase injury risk.', medium: 'Moderate training load; monitor closely.', low: 'Low training load; risk appears minimal.' }); updateGauge('#recovery-gauge', recoveryStatus, '#recovery-text', '#recovery-tooltip', { high: 'Excellent recovery time; low risk from this factor.', medium: 'Moderate recovery; consider increasing rest.', low: 'Insufficient recovery may elevate injury risk.' }); updateGauge('#injury-history-gauge', injuryImpact, '#injury-history-text', '#injury-history-tooltip', { high: 'Significant injury history; high risk of recurrence.', medium: 'Moderate injury history; take preventive measures.', low: 'Minimal injury history; low risk from this factor.' }); const recommendationListEl = document.querySelector('.recommendation-list'); const completedRecs = loadCompletedRecommendations(); recommendationListEl.innerHTML = Object.keys(enhancedRecommendations).map(category => `
${category}
${enhancedRecommendations[category].map(rec => ` `).join('')}
`).join(''); document.querySelectorAll('.rec-checkbox').forEach(checkbox => { checkbox.addEventListener('change', () => { const recId = checkbox.dataset.id; const completedRecs = loadCompletedRecommendations(); completedRecs[recId] = checkbox.checked; saveCompletedRecommendations(completedRecs); checkbox.closest('.recommendation-item').classList.toggle('completed', checkbox.checked); }); }); document.querySelectorAll('.recommendation-item.expandable').forEach(item => { item.addEventListener('click', (e) => { if (e.target.classList.contains('rec-checkbox') || e.target.classList.contains('learn-more-btn')) return; const detail = item.querySelector('.recommendation-detail'); if (detail.style.display === "none" || detail.style.display === "") { detail.style.display = "block"; detail.style.maxHeight = detail.scrollHeight + "px"; } else { detail.style.display = "none"; detail.style.maxHeight = "0"; } }); }); document.querySelectorAll('.learn-more-btn').forEach(btn => { btn.addEventListener('click', () => { const url = btn.dataset.url; window.open(url, '_blank'); }); }); resultContainer.style.display = 'block'; resultContainer.classList.remove("hidden"); setTimeout(() => { resultContainer.classList.add("visible"); }, 100); resultContainer.scrollIntoView({ behavior: "smooth" }); const reportActions = document.createElement('div'); reportActions.className = 'report-actions'; reportActions.innerHTML = ` `; resultContainer.appendChild(reportActions); } catch (err) { console.error('Prediction error:', err.message); const resultOutput = document.createElement('div'); resultOutput.innerHTML = `

Error: ${err.message}. Please ensure the prediction server is running and try again.

`; form.appendChild(resultOutput); } }); } });