pediatric-ai-scribe/public/js/milestones.js
ifedan-ed 993e46f720 Initial commit: Pediatric AI Scribe v1.0.0
- Live encounter recording → HPI generation
- Physician voice dictation → polished HPI
- Developmental milestones checklist (AAP/Nelson)
- 3-sentence summary generator
- OpenRouter integration with model selector
- Docker support
- Mobile responsive UI
2026-03-17 21:46:05 -04:00

301 lines
11 KiB
JavaScript

// ============================================================
// MODULE 3: DEVELOPMENTAL MILESTONES → NARRATIVE
// COMPLETELY SEPARATE FROM HPI MODULES 1 & 2
// Uses AI to convert checked milestones into sentences
// ============================================================
(function() {
var ageSelect = document.getElementById('ms-age-group');
var checklist = document.getElementById('milestone-checklist');
var actionsBar = document.getElementById('milestone-actions');
var allYesBtn = document.getElementById('ms-all-yes');
var allClearBtn = document.getElementById('ms-all-clear');
var generateBtn = document.getElementById('ms-generate-btn');
var outputCard = document.getElementById('ms-output');
var summaryBar = document.getElementById('ms-summary-bar');
var narrativeText = document.getElementById('ms-narrative-text');
var modelUsed = document.getElementById('ms-model-used');
// State: { "domain-0": { domain, milestone, status: null|'yes'|'no' } }
var state = {};
// When age group changes, rebuild checklist
ageSelect.addEventListener('change', function() {
var age = ageSelect.value;
state = {};
outputCard.classList.add('hidden');
if (!age || !MILESTONES_DATA[age]) {
checklist.innerHTML = '<p style="text-align:center;color:#9ca3af;padding:40px;">Select an age group to load milestones.</p>';
actionsBar.style.display = 'none';
return;
}
renderChecklist(MILESTONES_DATA[age]);
actionsBar.style.display = 'flex';
});
function renderChecklist(data) {
checklist.innerHTML = '';
Object.keys(data).forEach(function(domain) {
var items = data[domain];
var config = DOMAIN_CONFIG[domain] || { icon: '📋', css: '' };
var section = document.createElement('div');
section.className = 'domain-section ' + config.css;
var itemsHtml = '';
items.forEach(function(milestone, idx) {
var id = domain + '-' + idx;
state[id] = { domain: domain, milestone: milestone, status: null };
itemsHtml += '<div class="ms-item" id="item-' + safeId(id) + '">' +
'<div class="ms-toggle">' +
'<button class="toggle-btn" data-id="' + id + '" data-action="yes" title="Yes">✓</button>' +
'<button class="toggle-btn" data-id="' + id + '" data-action="no" title="No">✗</button>' +
'</div>' +
'<span class="ms-text">' + milestone + '</span>' +
'</div>';
});
section.innerHTML =
'<div class="domain-header">' +
'<span>' + config.icon + '</span>' +
'<span>' + domain + '</span>' +
'<span class="badge" id="badge-' + safeId(domain) + '">' + items.length + ' items</span>' +
'</div>' +
'<div class="ms-items">' + itemsHtml + '</div>';
checklist.appendChild(section);
});
// Attach click handlers
checklist.querySelectorAll('.toggle-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
handleToggle(btn.getAttribute('data-id'), btn.getAttribute('data-action'));
});
});
}
function safeId(str) {
return str.replace(/[^a-zA-Z0-9]/g, '_');
}
function handleToggle(id, action) {
var sid = safeId(id);
var item = document.getElementById('item-' + sid);
var yesBtn = item.querySelector('[data-action="yes"]');
var noBtn = item.querySelector('[data-action="no"]');
var current = state[id].status;
if (action === 'yes') {
if (current === 'yes') {
// Deselect
state[id].status = null;
yesBtn.classList.remove('yes-on');
item.classList.remove('is-yes', 'is-no');
} else {
state[id].status = 'yes';
yesBtn.classList.add('yes-on');
noBtn.classList.remove('no-on');
item.classList.add('is-yes');
item.classList.remove('is-no');
}
} else {
if (current === 'no') {
state[id].status = null;
noBtn.classList.remove('no-on');
item.classList.remove('is-yes', 'is-no');
} else {
state[id].status = 'no';
noBtn.classList.add('no-on');
yesBtn.classList.remove('yes-on');
item.classList.remove('is-yes');
item.classList.add('is-no');
}
}
updateBadges();
}
function updateBadges() {
var counts = {};
Object.keys(state).forEach(function(id) {
var d = state[id].domain;
if (!counts[d]) counts[d] = { total: 0, yes: 0, no: 0 };
counts[d].total++;
if (state[id].status === 'yes') counts[d].yes++;
if (state[id].status === 'no') counts[d].no++;
});
Object.keys(counts).forEach(function(domain) {
var badge = document.getElementById('badge-' + safeId(domain));
if (badge) {
var c = counts[domain];
var assessed = c.yes + c.no;
badge.textContent = assessed > 0
? c.yes + '✓ ' + c.no + '✗ / ' + c.total
: c.total + ' items';
}
});
}
// All Yes
allYesBtn.addEventListener('click', function() {
Object.keys(state).forEach(function(id) {
state[id].status = 'yes';
var sid = safeId(id);
var item = document.getElementById('item-' + sid);
if (item) {
item.querySelector('[data-action="yes"]').classList.add('yes-on');
item.querySelector('[data-action="no"]').classList.remove('no-on');
item.classList.add('is-yes');
item.classList.remove('is-no');
}
});
updateBadges();
showToast('All marked as achieved', 'success');
});
// Clear All
allClearBtn.addEventListener('click', function() {
Object.keys(state).forEach(function(id) {
state[id].status = null;
var sid = safeId(id);
var item = document.getElementById('item-' + sid);
if (item) {
item.querySelector('[data-action="yes"]').classList.remove('yes-on');
item.querySelector('[data-action="no"]').classList.remove('no-on');
item.classList.remove('is-yes', 'is-no');
}
});
updateBadges();
outputCard.classList.add('hidden');
showToast('All cleared', 'info');
});
// GENERATE NARRATIVE
// Sends ONLY assessed milestones to AI
// AI converts them into professional narrative sentences
generateBtn.addEventListener('click', function() {
var ageGroup = ageSelect.value;
if (!ageGroup) {
showToast('Select an age group first', 'error');
return;
}
// Collect all milestone data (AI will only use yes/no, ignore null)
var allMilestones = Object.keys(state).map(function(id) {
return state[id];
});
var assessed = allMilestones.filter(function(m) { return m.status !== null; });
if (assessed.length === 0) {
showToast('Please assess at least one milestone', 'error');
return;
}
showLoading('AI is generating developmental narrative...');
fetch('/api/generate-milestone-narrative', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
milestones: allMilestones,
ageGroup: ageGroup,
patientAge: document.getElementById('ms-patient-age').value,
patientGender: document.getElementById('ms-patient-gender').value,
model: getSelectedModel()
})
})
.then(function(r) { return r.json(); })
.then(function(data) {
hideLoading();
if (data.success) {
narrativeText.textContent = data.narrative;
modelUsed.textContent = (data.model || '').split('/').pop();
// Summary bar
if (data.summary) {
summaryBar.innerHTML =
'<div class="stat"><span class="dot dot-green"></span> Achieved: ' + data.summary.achieved + '</div>' +
'<div class="stat"><span class="dot dot-red"></span> Not Yet: ' + data.summary.notAchieved + '</div>' +
'<div class="stat"><span class="dot dot-gray"></span> Not Assessed: ' + data.summary.notAssessed + ' (omitted)</div>';
summaryBar.classList.remove('hidden');
}
outputCard.classList.remove('hidden');
outputCard.scrollIntoView({ behavior: 'smooth' });
showToast('Developmental narrative generated!', 'success');
} else {
showToast(data.error || 'Failed', 'error');
}
})
.catch(function(err) {
hideLoading();
showToast('Error: ' + err.message, 'error');
});
});
console.log('✅ Milestones module loaded (separate from HPI)');
// ============================================================
// 3-SENTENCE SUMMARY GENERATOR
// Takes the full narrative and condenses it
// ============================================================
var quickSummaryBtn = document.getElementById('ms-quick-summary-btn');
var quickSummaryDiv = document.getElementById('ms-quick-summary');
var summaryText = document.getElementById('ms-summary-text');
var summaryModelUsed = document.getElementById('ms-summary-model-used');
quickSummaryBtn.addEventListener('click', function() {
var fullNarrative = narrativeText.innerText.trim();
if (!fullNarrative) {
showToast('Generate the full narrative first', 'error');
return;
}
// Disable button and show loading state
quickSummaryBtn.disabled = true;
quickSummaryBtn.classList.add('loading');
var originalHTML = quickSummaryBtn.innerHTML;
quickSummaryBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Summarizing...';
fetch('/api/generate-milestone-summary', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
narrative: fullNarrative,
ageGroup: ageSelect.value,
patientAge: document.getElementById('ms-patient-age').value,
patientGender: document.getElementById('ms-patient-gender').value,
model: getSelectedModel()
})
})
.then(function(r) { return r.json(); })
.then(function(data) {
quickSummaryBtn.disabled = false;
quickSummaryBtn.classList.remove('loading');
quickSummaryBtn.innerHTML = '<i class="fas fa-compress-alt"></i> Regenerate Summary';
if (data.success) {
summaryText.textContent = data.summary;
summaryModelUsed.textContent = (data.model || '').split('/').pop();
quickSummaryDiv.classList.remove('hidden');
quickSummaryDiv.scrollIntoView({ behavior: 'smooth' });
showToast('3-sentence summary generated!', 'success');
} else {
quickSummaryBtn.innerHTML = originalHTML;
showToast(data.error || 'Summary failed', 'error');
}
})
.catch(function(err) {
quickSummaryBtn.disabled = false;
quickSummaryBtn.classList.remove('loading');
quickSummaryBtn.innerHTML = originalHTML;
showToast('Error: ' + err.message, 'error');
});
});
console.log('✅ 3-sentence summary feature loaded');
})();