- 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
301 lines
11 KiB
JavaScript
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');
|
|
})();
|