PlacementPredictor / script.js
ranilmukesh's picture
feat: update hostel and backlog handling in forms and backend logic
5f63ae0
/**
* PlacementPredictor+ Frontend Application
* AI-Powered Placement Prediction & Career Routing
*
* Connects to FastAPI backend for predictions and SHAP explanations
*/
// API Configuration
// Smart API Configuration: Use port 8000 locally, but use the current origin for remote (HF/Production)
const API_BASE_URL = (window.location.hostname === "127.0.0.1" || window.location.hostname === "localhost")
? window.location.protocol + "//" + window.location.hostname + ":7860"
: window.location.origin;
// DOM Elements
const elements = {
loadingOverlay: document.getElementById('loadingOverlay'),
studentForm: document.getElementById('patientForm'),
submitBtn: document.getElementById('submitBtn'),
resultsSection: document.getElementById('resultsSection'),
assessmentForm: document.getElementById('assessmentForm'),
backBtn: document.getElementById('backBtn'),
riskCard: document.getElementById('riskCard'),
riskPercentage: document.getElementById('riskPercentage'),
riskLevel: document.getElementById('riskLevel'),
riskConfidence: document.getElementById('riskConfidence'),
progressRing: document.getElementById('progressRing'),
meterPointer: document.getElementById('meterPointer'),
factorsContainer: document.getElementById('factorsContainer'),
recommendationsGrid: document.getElementById('recommendationsGrid'),
whatifSection: document.getElementById('whatifSection'),
whatifLoading: document.getElementById('whatifLoading'),
whatifScenariosGrid: document.getElementById('whatifScenariosGrid'),
whatifCombinedCard: document.getElementById('whatifCombinedCard'),
combinedOriginalRisk: document.getElementById('combinedOriginalRisk'),
combinedModifiedRisk: document.getElementById('combinedModifiedRisk'),
combinedDelta: document.getElementById('combinedDelta')
};
// State
let currentPrediction = null;
let currentExplanation = null;
let currentWhatIf = null;
let currentFormData = null;
let availableSkills = [];
let selectedSkills = [];
function encodeBacklogsFromCheckbox(checkboxId = 'backlogs') {
return document.getElementById(checkboxId).checked ? 1 : 0;
}
function decodeBacklogsToChecked(value) {
return Number(value) === 1;
}
function encodeHostelFromCheckbox(checkboxId = 'hostel') {
return document.getElementById(checkboxId).checked ? 1 : 0;
}
/**
* Initialize the application
*/
function init() {
setupEventListeners();
checkAPIHealth();
fetchOptions();
}
/**
* Fetch available streams and skills from API
*/
async function fetchOptions() {
try {
const response = await fetch(`${API_BASE_URL}/options`);
const data = await response.json();
const streamSelect = document.getElementById('stream');
streamSelect.innerHTML = '<option value="">Select stream</option>';
data.streams.forEach(stream => {
const opt = document.createElement('option');
opt.value = stream;
opt.textContent = stream;
streamSelect.appendChild(opt);
});
const roleSelect = document.getElementById('desired_role');
roleSelect.innerHTML = '<option value="">Select Target Job Role</option>';
if (data.jobs && data.jobs.length > 0) {
data.jobs.forEach(job => {
const opt = document.createElement('option');
opt.value = job;
opt.textContent = job;
roleSelect.appendChild(opt);
});
}
if (data.skills) {
availableSkills = data.skills;
setupSkillsAutocomplete();
}
} catch (e) {
console.error('Error fetching options:', e);
const streamSelect = document.getElementById('stream');
if (streamSelect) streamSelect.innerHTML = '<option value="">Error loading streams</option>';
const roleSelect = document.getElementById('desired_role');
if (roleSelect) roleSelect.innerHTML = '<option value="">Error loading roles</option>';
}
}
/**
* Setup Skills Autocomplete Logic
*/
function setupSkillsAutocomplete() {
const wrapper = document.getElementById('skillsWrapper');
const input = document.getElementById('skillsInput');
const hiddenInput = document.getElementById('skills');
const tagsContainer = document.getElementById('skillsTags');
const dropdown = document.getElementById('skillsDropdown');
function renderTags() {
tagsContainer.innerHTML = '';
selectedSkills.forEach((skill, index) => {
const tag = document.createElement('span');
tag.style.cssText = 'background: #047857; color: white; padding: 2px 8px; border-radius: 4px; font-size: 0.85rem; display: flex; align-items: center; gap: 4px;';
tag.innerHTML = `${skill} <span style="cursor:pointer;font-weight:bold;" onclick="removeSkill(${index})">&times;</span>`;
tagsContainer.appendChild(tag);
});
hiddenInput.value = selectedSkills.join(',');
}
window.removeSkill = function (index) {
selectedSkills.splice(index, 1);
renderTags();
};
function showDropdown(query = '') {
const filtered = availableSkills.filter(s => s.toLowerCase().includes(query.toLowerCase()) && !selectedSkills.includes(s));
dropdown.innerHTML = '';
if (filtered.length === 0) {
dropdown.style.display = 'none';
return;
}
filtered.slice(0, 50).forEach(skill => {
const div = document.createElement('div');
div.style.cssText = 'padding: 8px 12px; cursor: pointer; color: var(--text-color); border-bottom: 1px solid var(--gray-200);';
div.textContent = skill;
div.onmouseover = () => div.style.background = 'var(--gray-100)';
div.onmouseout = () => div.style.background = 'transparent';
div.onclick = () => {
selectedSkills.push(skill);
renderTags();
input.value = '';
dropdown.style.display = 'none';
input.focus();
};
dropdown.appendChild(div);
});
dropdown.style.display = 'block';
}
input.addEventListener('focus', () => showDropdown(input.value));
input.addEventListener('input', (e) => showDropdown(e.target.value));
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
const val = input.value.trim();
if (val && !selectedSkills.includes(val)) {
const actualSkill = availableSkills.find(s => s.toLowerCase() === val.toLowerCase());
if (actualSkill) {
selectedSkills.push(actualSkill);
renderTags();
input.value = '';
dropdown.style.display = 'none';
}
}
} else if (e.key === 'Backspace' && input.value === '' && selectedSkills.length > 0) {
selectedSkills.pop();
renderTags();
}
});
document.addEventListener('click', (e) => {
if (!wrapper.contains(e.target)) {
dropdown.style.display = 'none';
wrapper.style.borderColor = 'transparent';
}
});
wrapper.addEventListener('click', () => {
input.focus();
wrapper.style.borderColor = 'var(--black)';
});
}
/**
* Setup event listeners
*/
function setupEventListeners() {
// Form submission
elements.studentForm.addEventListener('submit', handleFormSubmit);
// Back button
elements.backBtn.addEventListener('click', showForm);
// Input animations
const inputs = document.querySelectorAll('input, select');
inputs.forEach(input => {
input.addEventListener('focus', () => {
input.closest('.input-group')?.classList.add('focused');
});
input.addEventListener('blur', () => {
input.closest('.input-group')?.classList.remove('focused');
});
});
}
/**
* Check API health status
*/
async function checkAPIHealth() {
try {
const response = await fetch(`${API_BASE_URL}/health`);
const data = await response.json();
if (!data.model_loaded) {
showNotification('Model not loaded. Please run train_model.py first.', 'warning');
}
} catch (error) {
showNotification('Cannot connect to API. Make sure the server is running.', 'error');
}
}
/**
* Handle form submission
*/
async function handleFormSubmit(e) {
e.preventDefault();
// Collect form data
const formData = collectFormData();
if (!validateFormData(formData)) {
return;
}
// Show loading
showLoading(true);
try {
// Make parallel API calls for prediction and explanation
const [predictionResult, explanationResult] = await Promise.all([
fetchPrediction(formData),
fetchExplanation(formData)
]);
currentPrediction = predictionResult;
currentExplanation = explanationResult;
currentFormData = formData;
// Display results
displayResults(predictionResult, explanationResult);
// Fetch What-If analysis in background (non-blocking)
fetchWhatIfAnalysis(formData);
} catch (error) {
console.error('API Error:', error);
showNotification('Failed to get prediction. Please try again.', 'error');
showLoading(false);
}
}
/**
* Collect form data
*/
function collectFormData() {
return {
Gender: document.getElementById('gender').value,
Age: parseInt(document.getElementById('age').value),
Stream: document.getElementById('stream').value,
Internships: parseInt(document.getElementById('internships').value) || 0,
CGPA: parseFloat(document.getElementById('cgpa').value) || 0,
Hostel: encodeHostelFromCheckbox('hostel'),
HistoryOfBacklogs: encodeBacklogsFromCheckbox('backlogs'),
skills: document.getElementById('skills').value.split(',').map(s => s.trim()).filter(s => s),
desired_role: document.getElementById('desired_role').value || null
};
}
/**
* Validate form data
*/
function validateFormData(data) {
const requiredFields = ['Gender', 'Age', 'Stream', 'Internships', 'CGPA'];
for (const field of requiredFields) {
if (!data[field] && data[field] !== 0) {
showNotification(`Please fill in all required fields.`, 'warning');
return false;
}
}
if (data.Age < 15 || data.Age > 50) {
showNotification('Please enter a valid age (15-50).', 'warning');
return false;
}
if (data.CGPA < 0 || data.CGPA > 10) {
showNotification('Please enter a valid CGPA (0-10).', 'warning');
return false;
}
return true;
}
/**
* Fetch prediction from API
*/
async function fetchPrediction(data) {
const response = await fetch(`${API_BASE_URL}/predict`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
/**
* Fetch explanation from API
*/
async function fetchExplanation(data) {
const response = await fetch(`${API_BASE_URL}/explain`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
/**
* Display results
*/
function displayResults(prediction, explanation) {
// Hide loading after a short delay for smooth transition
setTimeout(() => {
showLoading(false);
// Switch views
elements.assessmentForm.classList.add('hidden');
elements.resultsSection.classList.remove('hidden');
// Scroll to top smoothly
window.scrollTo({ top: 0, behavior: 'smooth' });
// Animate results
setTimeout(() => {
animateRiskScore(prediction);
displayFactors(explanation.top_contributing_factors);
displayRecommendations(prediction.risk_level);
// Render Graph Image if present
if (prediction.graph_data) {
document.getElementById('graphSection').style.display = 'block';
document.getElementById('graphImage').src = prediction.graph_data;
} else {
document.getElementById('graphSection').style.display = 'none';
}
// Initialize Risk Simulator
initSimulator();
// Show chat widget after results are displayed
showChatWidget();
}, 300);
}, 1000);
}
/**
* Animate risk score display
*/
function animateRiskScore(prediction) {
const percentage = prediction.probability_percentage;
const riskLevel = prediction.risk_level;
// Set risk card class
elements.riskCard.className = 'risk-card ' + riskLevel.toLowerCase();
// Update confidence
elements.riskConfidence.textContent = prediction.confidence;
// Animate percentage counter
animateCounter(elements.riskPercentage, 0, percentage, 1500);
// Animate progress ring
const circumference = 2 * Math.PI * 54; // r = 54
const offset = circumference - (percentage / 100) * circumference;
setTimeout(() => {
elements.progressRing.style.strokeDashoffset = offset;
}, 100);
// Animate risk level text
setTimeout(() => {
elements.riskLevel.textContent = riskLevel;
}, 500);
// Animate meter pointer
setTimeout(() => {
elements.meterPointer.style.left = `${percentage}%`;
}, 100);
}
/**
* Animate counter from start to end
*/
function animateCounter(element, start, end, duration) {
const startTime = performance.now();
const diff = end - start;
function update(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// Easing function (ease-out)
const easeOut = 1 - Math.pow(1 - progress, 3);
const current = start + diff * easeOut;
element.textContent = current.toFixed(1);
if (progress < 1) {
requestAnimationFrame(update);
}
}
requestAnimationFrame(update);
}
/**
* Display factor cards
*/
function displayFactors(factors) {
elements.factorsContainer.innerHTML = '';
// Find max impact for normalization
const maxImpact = Math.max(...factors.map(f => Math.abs(f.impact)));
factors.forEach((factor, index) => {
const card = createFactorCard(factor, maxImpact);
elements.factorsContainer.appendChild(card);
// Trigger animation
setTimeout(() => {
card.classList.add('animate');
}, 50);
});
}
/**
* Create a factor card element
*/
function createFactorCard(factor, maxImpact) {
const card = document.createElement('div');
card.className = 'factor-card';
const isPositive = factor.impact > 0;
const normalizedImpact = (Math.abs(factor.impact) / maxImpact) * 100;
const featureName = formatFeatureName(factor.feature);
card.innerHTML = `
<div class="factor-header">
<span class="factor-name">${featureName}</span>
<span class="factor-direction ${isPositive ? 'increases' : 'reduces'}">
${factor.direction}
</span>
</div>
<div class="factor-bar">
<div class="factor-bar-fill ${isPositive ? 'positive' : 'negative'}"
style="width: 0%"
data-width="${normalizedImpact}%"></div>
</div>
`;
// Animate bar after card is added
setTimeout(() => {
const bar = card.querySelector('.factor-bar-fill');
bar.style.width = `${normalizedImpact}%`;
}, 300);
return card;
}
/**
* Format feature name for display
*/
function formatFeatureName(name) {
// Handle common feature name patterns
const nameMap = {
'Age': 'Student Age',
'Gender_Female': 'Gender: Female',
'Gender_Male': 'Gender: Male',
'Internships': 'Number of Internships',
'CGPA': 'Cumulative GPA',
'Hostel': 'Hostel Accommodation',
'HistoryOfBacklogs': 'Academic Backlogs'
};
// For streams, handle dynamically
if (name.startsWith('Stream_')) {
return 'Stream: ' + name.replace('Stream_', '').replace(/_/g, ' ');
}
return nameMap[name] || name.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
}
/**
* Display recommendations based on risk level
*/
function displayRecommendations(riskLevel) {
const recommendations = getRecommendations(riskLevel);
elements.recommendationsGrid.innerHTML = '';
recommendations.forEach(rec => {
const card = document.createElement('div');
// If it's the Career Path recommendation, add a special class
card.className = `recommendation-card ${rec.title === 'Career Path' ? 'highlight-card' : ''}`;
card.innerHTML = `
<div class="recommendation-icon">${rec.icon}</div>
<h4 class="recommendation-title">${rec.title}</h4>
<p class="recommendation-text">${rec.text}</p>
`;
elements.recommendationsGrid.appendChild(card);
});
}
/**
* Get recommendations based on risk level
*/
function getRecommendations(riskLevel) {
// Added recommended job functionality here
const jobRec = currentPrediction?.recommended_job ? {
icon: '🎯',
title: 'Career Path',
text: `Based on your specific skill profile, your ideal career path is <strong style="color:#ffd700; font-size:1.15em; text-decoration:underline;">${currentPrediction.recommended_job}</strong>.`
} : null;
// Added missing skills recommendation
const skillsRec = currentPrediction?.missing_skills && currentPrediction.missing_skills.length > 0 ? {
icon: '🛠️',
title: 'Skill Gaps',
// Show full list of missing skills rather than truncating
text: `Focus on mastering: ${currentPrediction.missing_skills.join(', ')}`
} : null;
let base = [
{
icon: '📚',
title: 'Continuous Learning',
text: 'Keep building projects to stand out to recruiters.'
}
];
if (jobRec) base.unshift(jobRec);
if (skillsRec) base.push(skillsRec);
if (riskLevel === 'HIGH') {
return [
{
icon: '🛠️',
title: 'Skill Development',
text: 'Focus heavily on matching industry required skills.'
},
{
icon: '📈',
title: 'Improve Academics',
text: 'Work on your CGPA and try to secure internships.'
},
...base
];
} else if (riskLevel === 'MEDIUM') {
return [
{
icon: '🤝',
title: 'Networking',
text: 'Connect with alumni and professionals in your target field.'
},
...base
];
} else {
return [
{
icon: '🚀',
title: 'Prepare for Interviews',
text: 'You are in a great position. Start practicing mock interviews!'
},
...base
];
}
}
/**
* Show/hide loading overlay
*/
function showLoading(show) {
if (show) {
elements.loadingOverlay.classList.add('active');
} else {
elements.loadingOverlay.classList.remove('active');
}
}
/**
* Show form and hide results
*/
function showForm() {
elements.resultsSection.classList.add('hidden');
elements.assessmentForm.classList.remove('hidden');
// Reset form
elements.patientForm.reset();
// Reset What-If state
resetWhatIf();
// Hide chat widget
hideChatWidget();
// Scroll to form
window.scrollTo({ top: 0, behavior: 'smooth' });
}
/**
* Show notification toast
*/
function showNotification(message, type = 'info') {
// Remove existing notifications
const existing = document.querySelector('.notification');
if (existing) {
existing.remove();
}
// Create notification element
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.innerHTML = `
<span class="notification-message">${message}</span>
<button class="notification-close">&times;</button>
`;
// Add styles dynamically
notification.style.cssText = `
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: ${type === 'error' ? '#E85D4C' : type === 'warning' ? '#FF9800' : '#2D9596'};
color: white;
padding: 12px 24px;
border-radius: 8px;
display: flex;
align-items: center;
gap: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 10000;
max-width: 90%;
animation: slideUp 0.3s ease;
`;
// Add animation keyframes
const style = document.createElement('style');
style.textContent = `
@keyframes slideUp {
from {
opacity: 0;
transform: translateX(-50%) translateY(20px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
`;
document.head.appendChild(style);
document.body.appendChild(notification);
// Close button handler
notification.querySelector('.notification-close').addEventListener('click', () => {
notification.remove();
});
// Auto-dismiss after 5 seconds
setTimeout(() => {
notification.style.opacity = '0';
notification.style.transform = 'translateX(-50%) translateY(20px)';
notification.style.transition = 'all 0.3s ease';
setTimeout(() => notification.remove(), 300);
}, 5000);
}
/**
* Demo mode - fill form with sample data
*/
function fillDemoData() {
document.getElementById('gender').value = 'Female';
document.getElementById('age').value = '21';
document.getElementById('stream').value = 'Information Technology';
document.getElementById('internships').value = '1';
document.getElementById('cgpa').value = '7.5';
document.getElementById('hostel').checked = true;
document.getElementById('backlogs').checked = true; // Highlighting improvement area
// Setup skills tags visually and hidden
selectedSkills = ['Python', 'SQL', 'Git'];
const hiddenInput = document.getElementById('skills');
const tagsContainer = document.getElementById('skillsTags');
if (tagsContainer && hiddenInput) {
tagsContainer.innerHTML = '';
selectedSkills.forEach((skill, index) => {
const tag = document.createElement('span');
tag.style.cssText = 'background: #047857; color: white; padding: 2px 8px; border-radius: 4px; font-size: 0.85rem; display: flex; align-items: center; gap: 4px;';
tag.innerHTML = `${skill} <span style="cursor:pointer;font-weight:bold;" onclick="removeSkill(${index})">&times;</span>`;
tagsContainer.appendChild(tag);
});
hiddenInput.value = selectedSkills.join(',');
}
// Attempt to set desired role if options exist
const roleOpt = Array.from(document.getElementById('desired_role').options).find(opt => opt.value === 'Data Analyst');
if (roleOpt) {
document.getElementById('desired_role').value = 'Data Analyst';
}
}
/**
* =============================================
* WHAT-IF SCENARIO ANALYSIS
* =============================================
*/
/**
* Fetch What-If analysis from API
*/
async function fetchWhatIfAnalysis(formData) {
// Show the What-If section with loading state
elements.whatifSection.style.display = 'block';
elements.whatifLoading.style.display = 'flex';
elements.whatifScenariosGrid.innerHTML = '';
elements.whatifCombinedCard.style.display = 'none';
try {
const response = await fetch(`${API_BASE_URL}/whatif`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const whatifData = await response.json();
currentWhatIf = whatifData;
// Hide loading, display results
elements.whatifLoading.style.display = 'none';
displayWhatIfAnalysis(whatifData);
} catch (error) {
console.error('What-If API Error:', error);
elements.whatifLoading.style.display = 'none';
elements.whatifScenariosGrid.innerHTML = `
<div class="whatif-error">
<p>⚠️ Could not generate What-If scenarios. The analysis will still work without this feature.</p>
</div>
`;
}
}
/**
* Display What-If analysis results
*/
function displayWhatIfAnalysis(data) {
if (!data.scenarios || data.scenarios.length === 0) {
elements.whatifScenariosGrid.innerHTML = `
<div class="whatif-empty">
<p>✅ Your current health parameters are already in healthy ranges. No significant changes to suggest!</p>
</div>
`;
return;
}
// Render scenario cards with staggered animation
data.scenarios.forEach((scenario, index) => {
const card = createScenarioCard(scenario);
elements.whatifScenariosGrid.appendChild(card);
// Staggered fade-in animation
setTimeout(() => {
card.classList.add('animate');
}, 100 + index * 150);
});
// Show combined outcome if available
if (data.combined_risk !== null && data.combined_risk !== undefined && data.scenarios.length > 1) {
setTimeout(() => {
displayCombinedOutcome(data);
}, 100 + data.scenarios.length * 150 + 200);
}
}
/**
* Create a single scenario card
*/
function createScenarioCard(scenario) {
const card = document.createElement('div');
card.className = 'whatif-card';
const isReduction = scenario.risk_delta > 0;
const deltaAbs = Math.abs(scenario.risk_delta).toFixed(1);
const reductionPct = Math.abs(scenario.risk_reduction_percent).toFixed(1);
// Determine the bar width (scale delta to percentage of original risk for visualization)
const barWidth = Math.min(Math.abs(scenario.risk_reduction_percent), 100);
card.innerHTML = `
<div class="whatif-card-icon">${scenario.icon}</div>
<div class="whatif-card-content">
<h4 class="whatif-card-title">${scenario.title}</h4>
<p class="whatif-card-desc">${scenario.description}</p>
<div class="whatif-card-comparison">
<div class="whatif-risk-original">
<span class="whatif-risk-label">Current</span>
<span class="whatif-risk-val">${scenario.original_risk.toFixed(1)}%</span>
</div>
<div class="whatif-arrow-container">
<svg class="whatif-arrow ${isReduction ? 'reduction' : 'increase'}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 12h14M12 5l7 7-7 7" />
</svg>
</div>
<div class="whatif-risk-modified">
<span class="whatif-risk-label">Modified</span>
<span class="whatif-risk-val ${isReduction ? 'improved' : 'worsened'}">${scenario.modified_risk.toFixed(1)}%</span>
</div>
</div>
<div class="whatif-delta-bar">
<div class="whatif-delta-fill ${isReduction ? 'positive' : 'negative'}"
style="width: 0%" data-width="${barWidth}%"></div>
</div>
<div class="whatif-delta-text ${isReduction ? 'positive' : 'negative'}">
${isReduction ? '↓' : '↑'} ${deltaAbs}% risk ${isReduction ? 'reduction' : 'increase'}
<span class="whatif-delta-pct">(${reductionPct}% ${isReduction ? 'improvement' : 'change'})</span>
</div>
</div>
`;
// Animate the delta bar after card is rendered
setTimeout(() => {
const bar = card.querySelector('.whatif-delta-fill');
if (bar) {
bar.style.width = `${barWidth}%`;
}
}, 500);
return card;
}
/**
* Display combined best-case outcome card
*/
function displayCombinedOutcome(data) {
elements.whatifCombinedCard.style.display = 'block';
const originalRisk = data.original_risk;
const combinedRisk = data.combined_risk;
const totalDelta = originalRisk - combinedRisk;
const totalReductionPct = originalRisk > 0 ? (totalDelta / originalRisk * 100) : 0;
elements.combinedOriginalRisk.textContent = `${originalRisk.toFixed(1)}%`;
elements.combinedModifiedRisk.textContent = `${combinedRisk.toFixed(1)}%`;
// Color the modified risk value
if (combinedRisk < originalRisk) {
elements.combinedModifiedRisk.classList.add('improved');
}
// Delta summary
const isReduction = totalDelta > 0;
elements.combinedDelta.innerHTML = `
<div class="combined-delta-badge ${isReduction ? 'positive' : 'negative'}">
${isReduction ? '↓' : '↑'} ${Math.abs(totalDelta).toFixed(1)}% total risk ${isReduction ? 'reduction' : 'increase'}
<span>(${Math.abs(totalReductionPct).toFixed(1)}% overall ${isReduction ? 'improvement' : 'change'})</span>
</div>
<p class="combined-risk-level">Risk Level: <strong>${data.original_risk_level}</strong> → <strong class="${(data.combined_risk_level || '').toLowerCase()}">${data.combined_risk_level || 'N/A'}</strong></p>
`;
// Animate in
setTimeout(() => {
elements.whatifCombinedCard.classList.add('animate');
}, 100);
}
/**
* Reset What-If section state
*/
function resetWhatIf() {
currentWhatIf = null;
elements.whatifSection.style.display = 'none';
elements.whatifScenariosGrid.innerHTML = '';
elements.whatifCombinedCard.style.display = 'none';
elements.whatifCombinedCard.classList.remove('animate');
elements.combinedModifiedRisk.classList.remove('improved');
}
/**
* =============================================
* INTERACTIVE RISK SIMULATOR
* =============================================
*/
let simTimeout = null;
function initSimulator() {
if (!currentFormData || !currentPrediction) return;
// Show the simulator section
const simCard = document.getElementById('interactiveSimulator');
if (simCard) simCard.classList.remove('hidden');
// Populate baseline stats
const baselineRiskStr = currentPrediction.probability_percentage.toFixed(1) + '%';
document.getElementById('simBaselineStat').textContent = baselineRiskStr;
// Initial UI update with baseline data
updateSimulatorUI(currentPrediction, currentPrediction, currentFormData);
// Setup Event Listeners
setupSimControl('Age', 'Age', 15, 50);
setupSimControl('Intern', 'Internships', 0, 10);
setupSimControl('CGPA', 'CGPA', 0, 10);
// Toggle Setup
const toggle = document.getElementById('simBacklogToggle');
toggle.checked = decodeBacklogsToChecked(currentFormData.HistoryOfBacklogs);
toggle.onchange = () => { triggerSimulate(); };
}
function setupSimControl(idPrefix, fieldName, min, max) {
const slider = document.getElementById(`sim${idPrefix}Slider`);
const num = document.getElementById(`sim${idPrefix}Num`);
if (slider && num && currentFormData) {
let val = Number(currentFormData[fieldName]) || min;
if (val < min) val = min;
if (val > max) val = max;
slider.value = val;
num.value = val;
slider.oninput = (e) => { num.value = e.target.value; triggerSimulate(); };
num.oninput = (e) => {
let v = Number(e.target.value);
if (v >= min && v <= max) { slider.value = v; triggerSimulate(); }
};
}
}
function triggerSimulate() {
clearTimeout(simTimeout);
simTimeout = setTimeout(simulateRisk, 300); // 300ms debounce
}
async function simulateRisk() {
if (!currentFormData) return;
// Create new student data object
const simulatedData = { ...currentFormData };
// Override with simulator values
simulatedData.Age = parseInt(document.getElementById('simAgeNum').value);
simulatedData.Internships = parseInt(document.getElementById('simInternNum').value);
simulatedData.CGPA = parseFloat(document.getElementById('simCGPANum').value);
simulatedData.HistoryOfBacklogs = encodeBacklogsFromCheckbox('simBacklogToggle');
try {
const response = await fetchPrediction(simulatedData);
updateSimulatorUI(currentPrediction, response, simulatedData);
} catch (e) {
console.error("Simulation failed", e);
}
}
function updateSimulatorUI(baselinePred, targetPred, simData) {
const risk = targetPred.probability_percentage;
const levelStr = targetPred.risk_level.toUpperCase();
// Text values
document.getElementById('simRiskValue').textContent = risk.toFixed(1) + '%';
document.getElementById('simRiskLabel').textContent = levelStr + ' RISK';
document.getElementById('simTargetStat').textContent = risk.toFixed(1) + '%';
// Delta
const delta = (risk - baselinePred.probability_percentage).toFixed(1);
const deltaEl = document.getElementById('simDeltaStat');
deltaEl.textContent = (delta > 0 ? '+' : '') + delta + '%';
deltaEl.className = 'sim-stat-val ' + (delta > 0 ? 'negative' : (delta < 0 ? 'positive' : ''));
// Dynamic Arc length = 219.91
const trackFilled = 219.91 * (1 - (risk / 100)); // risk mapped as 0-100%
const simTrack = document.getElementById('simTrack');
if (simTrack) {
simTrack.style.strokeDashoffset = trackFilled;
// Color
let color = 'var(--risk-low)';
if (targetPred.risk_level === 'MEDIUM') color = 'var(--risk-medium)';
if (targetPred.risk_level === 'HIGH') color = 'var(--risk-high)';
simTrack.style.stroke = color;
}
// Baseline Marker (Angle 0 is left, angle 180 is right)
const baseRisk = baselinePred.probability_percentage;
const baseAngle = 180 * (baseRisk / 100);
const baseGroup = document.getElementById('simBaselineGroup');
const baseTextGroup = document.getElementById('simBaselineTextGroup');
if (baseGroup) baseGroup.style.transform = `rotate(${baseAngle}deg)`;
if (baseTextGroup) baseTextGroup.style.transform = `translate(18px, 90px) rotate(${-baseAngle}deg)`;
// Target Marker
const targetAngle = 180 * (risk / 100);
const targetGroup = document.getElementById('simTargetGroup');
const targetTextGroup = document.getElementById('simTargetTextGroup');
if (targetGroup) targetGroup.style.transform = `rotate(${targetAngle}deg)`;
if (targetTextGroup) targetTextGroup.style.transform = `translate(18px, 110px) rotate(${-targetAngle}deg)`;
}
/**
* =============================================
* AI CHAT FUNCTIONALITY
* =============================================
*/
let chatSessionId = null;
let chatIsOpen = false;
let chatInitialized = false;
let chatElems = {};
function initChatElements() {
chatElems = {
widget: document.getElementById('chatWidget'),
toggle: document.getElementById('chatToggle'),
panel: document.getElementById('chatPanel'),
minimize: document.getElementById('chatMinimize'),
messages: document.getElementById('chatMessages'),
input: document.getElementById('chatInput'),
send: document.getElementById('chatSend'),
status: document.getElementById('chatStatus'),
iconOpen: document.querySelector('.chat-icon-open'),
iconClose: document.querySelector('.chat-icon-close'),
};
chatElems.toggle.addEventListener('click', toggleChat);
chatElems.minimize.addEventListener('click', toggleChat);
chatElems.send.addEventListener('click', sendChatMessage);
chatElems.input.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendChatMessage();
}
});
chatElems.input.addEventListener('input', () => {
chatElems.send.disabled = !chatElems.input.value.trim();
});
}
function showChatWidget() {
if (!chatElems.widget) initChatElements();
chatElems.widget.style.display = 'block';
}
function hideChatWidget() {
if (!chatElems.widget) return;
chatElems.widget.style.display = 'none';
chatElems.panel.style.display = 'none';
chatIsOpen = false;
chatInitialized = false;
chatSessionId = null;
if (chatElems.messages) chatElems.messages.innerHTML = '';
if (chatElems.iconOpen) chatElems.iconOpen.style.display = 'block';
if (chatElems.iconClose) chatElems.iconClose.style.display = 'none';
}
function toggleChat() {
chatIsOpen = !chatIsOpen;
chatElems.panel.style.display = chatIsOpen ? 'flex' : 'none';
chatElems.iconOpen.style.display = chatIsOpen ? 'none' : 'block';
chatElems.iconClose.style.display = chatIsOpen ? 'block' : 'none';
if (chatIsOpen && !chatInitialized) {
initializeChat();
}
if (chatIsOpen) {
chatElems.input.focus();
}
}
async function initializeChat() {
chatInitialized = true;
addTypingIndicator();
setChatStatus('Connecting...');
const formData = collectFormData();
try {
const response = await fetch(`${API_BASE_URL}/chat/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
patient_data: formData,
prediction: currentPrediction,
explanation: currentExplanation,
whatif: currentWhatIf || {}
})
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
chatSessionId = data.session_id;
removeTypingIndicator();
addChatMessage('ai', data.message);
setChatStatus('Online');
} catch (error) {
console.error('Chat init error:', error);
removeTypingIndicator();
addChatMessage('system',
'⚠️ Could not connect to Placement AI. Make sure NVIDIA_API_KEY is set and agno is installed.'
);
setChatStatus('Offline');
}
}
async function sendChatMessage() {
const message = chatElems.input.value.trim();
if (!message || !chatSessionId) return;
addChatMessage('user', message);
chatElems.input.value = '';
chatElems.send.disabled = true;
addTypingIndicator();
setChatStatus('Thinking...');
try {
const response = await fetch(`${API_BASE_URL}/chat/message`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
session_id: chatSessionId,
message: message
})
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
removeTypingIndicator();
addChatMessage('ai', data.response);
setChatStatus('Online');
} catch (error) {
console.error('Chat error:', error);
removeTypingIndicator();
addChatMessage('system', '⚠️ Failed to get a response. Please try again.');
setChatStatus('Online');
}
}
function addChatMessage(type, text) {
const msgDiv = document.createElement('div');
msgDiv.className = `chat-msg chat-msg-${type}`;
const bubble = document.createElement('div');
bubble.className = 'chat-bubble';
bubble.innerHTML = formatChatMarkdown(text);
msgDiv.appendChild(bubble);
chatElems.messages.appendChild(msgDiv);
chatElems.messages.scrollTop = chatElems.messages.scrollHeight;
}
function addTypingIndicator() {
if (document.getElementById('chatTyping')) return;
const indicator = document.createElement('div');
indicator.id = 'chatTyping';
indicator.className = 'chat-msg chat-msg-ai';
indicator.innerHTML = `
<div class="chat-bubble typing-indicator">
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
`;
chatElems.messages.appendChild(indicator);
chatElems.messages.scrollTop = chatElems.messages.scrollHeight;
}
function removeTypingIndicator() {
const el = document.getElementById('chatTyping');
if (el) el.remove();
}
function setChatStatus(text) {
if (chatElems.status) {
chatElems.status.innerHTML = `<span class="status-dot"></span> ${text}`;
}
}
function formatChatMarkdown(text) {
if (!text) return '';
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/`(.+?)`/g, '<code>$1</code>')
.replace(/^\s*[-•]\s+(.+)/gm, '<li>$1</li>')
.replace(/^\s*(\d+)\.\s+(.+)/gm, '<li>$2</li>')
.replace(/\n/g, '<br>');
}
// Start the app when the page is loaded
document.addEventListener('DOMContentLoaded', init);